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.
Files changed (141) hide show
  1. package/App.tsx +182 -0
  2. package/README.md +330 -0
  3. package/assets/xertica-logo.svg +38 -0
  4. package/assets/xertica-x-logo.svg +21 -0
  5. package/bin/cli.ts +193 -0
  6. package/components/AssistenteXertica.tsx +2003 -0
  7. package/components/AudioPlayer.tsx +203 -0
  8. package/components/CodeBlock.tsx +242 -0
  9. package/components/DocumentEditor.tsx +504 -0
  10. package/components/ForgotPasswordPage.tsx +170 -0
  11. package/components/FormattedDocument.tsx +87 -0
  12. package/components/HomeContent.tsx +123 -0
  13. package/components/HomePage.tsx +70 -0
  14. package/components/LanguageSelector.tsx +54 -0
  15. package/components/LoginPage.tsx +199 -0
  16. package/components/MarkdownMessage.tsx +62 -0
  17. package/components/ModernChatInput.tsx +502 -0
  18. package/components/PodcastPlayer.tsx +409 -0
  19. package/components/ResetPasswordPage.tsx +234 -0
  20. package/components/Sidebar.tsx +489 -0
  21. package/components/TemplateContent.tsx +629 -0
  22. package/components/TemplatePage.tsx +70 -0
  23. package/components/ThemeToggle.tsx +65 -0
  24. package/components/VerifyEmailPage.tsx +187 -0
  25. package/components/XerticaLogo.tsx +69 -0
  26. package/components/XerticaOrbe.tsx +1339 -0
  27. package/components/XerticaXLogo.tsx +53 -0
  28. package/components/examples/DrawingMapExample.tsx +530 -0
  29. package/components/examples/FilterableMapExample.tsx +380 -0
  30. package/components/examples/LocationPickerExample.tsx +330 -0
  31. package/components/examples/MapExamples.tsx +280 -0
  32. package/components/examples/MapShowcase.tsx +446 -0
  33. package/components/examples/RouteMapExamples.tsx +329 -0
  34. package/components/examples/SimpleFilterableMap.tsx +192 -0
  35. package/components/examples/index.ts +52 -0
  36. package/components/figma/ImageWithFallback.tsx +27 -0
  37. package/components/index.ts +44 -0
  38. package/components/media/AudioPlayer.tsx +278 -0
  39. package/components/media/FloatingMediaWrapper.tsx +166 -0
  40. package/components/media/VideoPlayer.tsx +285 -0
  41. package/components/ui/accordion.tsx +66 -0
  42. package/components/ui/alert-dialog.tsx +159 -0
  43. package/components/ui/alert.tsx +91 -0
  44. package/components/ui/aspect-ratio.tsx +11 -0
  45. package/components/ui/avatar.tsx +65 -0
  46. package/components/ui/badge.tsx +55 -0
  47. package/components/ui/breadcrumb.tsx +109 -0
  48. package/components/ui/button.tsx +78 -0
  49. package/components/ui/calendar.tsx +235 -0
  50. package/components/ui/card.tsx +92 -0
  51. package/components/ui/carousel.tsx +241 -0
  52. package/components/ui/chart.tsx +353 -0
  53. package/components/ui/checkbox.tsx +32 -0
  54. package/components/ui/collapsible.tsx +33 -0
  55. package/components/ui/command.tsx +177 -0
  56. package/components/ui/context-menu.tsx +252 -0
  57. package/components/ui/dialog.tsx +138 -0
  58. package/components/ui/drawer.tsx +134 -0
  59. package/components/ui/dropdown-menu.tsx +257 -0
  60. package/components/ui/empty.tsx +90 -0
  61. package/components/ui/file-upload.tsx +152 -0
  62. package/components/ui/form.tsx +195 -0
  63. package/components/ui/google-maps-loader.tsx +379 -0
  64. package/components/ui/hover-card.tsx +44 -0
  65. package/components/ui/index.ts +242 -0
  66. package/components/ui/input-otp.tsx +77 -0
  67. package/components/ui/input.tsx +38 -0
  68. package/components/ui/label.tsx +24 -0
  69. package/components/ui/map-config.ts +12 -0
  70. package/components/ui/map-layers.tsx +129 -0
  71. package/components/ui/map.exports.ts +31 -0
  72. package/components/ui/map.tsx +412 -0
  73. package/components/ui/menubar.tsx +276 -0
  74. package/components/ui/navigation-menu.tsx +162 -0
  75. package/components/ui/notification-badge.tsx +61 -0
  76. package/components/ui/page-header.tsx +229 -0
  77. package/components/ui/pagination.tsx +127 -0
  78. package/components/ui/popover.tsx +48 -0
  79. package/components/ui/progress.tsx +31 -0
  80. package/components/ui/radio-group.tsx +56 -0
  81. package/components/ui/rating.tsx +102 -0
  82. package/components/ui/resizable.tsx +405 -0
  83. package/components/ui/route-map.tsx +246 -0
  84. package/components/ui/scroll-area.tsx +58 -0
  85. package/components/ui/search.tsx +70 -0
  86. package/components/ui/select.tsx +176 -0
  87. package/components/ui/separator.tsx +28 -0
  88. package/components/ui/sheet.tsx +138 -0
  89. package/components/ui/sidebar.tsx +726 -0
  90. package/components/ui/simple-map.tsx +92 -0
  91. package/components/ui/skeleton.tsx +13 -0
  92. package/components/ui/slider.tsx +58 -0
  93. package/components/ui/sonner.tsx +77 -0
  94. package/components/ui/stats-card.tsx +84 -0
  95. package/components/ui/stepper.tsx +126 -0
  96. package/components/ui/switch.tsx +34 -0
  97. package/components/ui/table.tsx +116 -0
  98. package/components/ui/tabs.tsx +66 -0
  99. package/components/ui/textarea.tsx +26 -0
  100. package/components/ui/timeline.tsx +140 -0
  101. package/components/ui/toggle-group.tsx +71 -0
  102. package/components/ui/toggle.tsx +46 -0
  103. package/components/ui/tooltip.tsx +61 -0
  104. package/components/ui/tree-view.tsx +123 -0
  105. package/components/ui/use-mobile.ts +24 -0
  106. package/components/ui/utils.ts +6 -0
  107. package/components/ui/xertica-assistant.tsx +1420 -0
  108. package/contexts/ApiKeyContext.tsx +123 -0
  109. package/contexts/AssistenteContext.tsx +118 -0
  110. package/contexts/BrandColorsContext.tsx +551 -0
  111. package/contexts/LanguageContext.tsx +36 -0
  112. package/contexts/ThemeContext.tsx +85 -0
  113. package/dist/cli.js +20922 -0
  114. package/eslint.config.js +41 -0
  115. package/guidelines/Guidelines.md +61 -0
  116. package/hooks/useTheme.ts +4 -0
  117. package/imports/Podcast.tsx +389 -0
  118. package/imports/XerticaAi.tsx +46 -0
  119. package/imports/XerticaX.tsx +20 -0
  120. package/imports/svg-aueiaqngck.ts +11 -0
  121. package/imports/svg-v9krss1ozd.ts +16 -0
  122. package/imports/svg-vhrdofe3qe.ts +5 -0
  123. package/index.css +4448 -0
  124. package/index.html +14 -0
  125. package/main.tsx +10 -0
  126. package/package.json +119 -0
  127. package/postcss.config.js +6 -0
  128. package/routes.tsx +33 -0
  129. package/styles/globals.css +15 -0
  130. package/styles/xertica/app-overrides/chat.css +61 -0
  131. package/styles/xertica/app-overrides/scrollbar.css +33 -0
  132. package/styles/xertica/base.css +70 -0
  133. package/styles/xertica/integrations/google-maps.css +76 -0
  134. package/styles/xertica/integrations/sonner.css +73 -0
  135. package/styles/xertica/theme-map.css +88 -0
  136. package/styles/xertica/tokens.css +190 -0
  137. package/tsconfig.json +31 -0
  138. package/tsconfig.node.json +10 -0
  139. package/utils/gemini.ts +140 -0
  140. package/vite-env.d.ts +12 -0
  141. package/vite.config.ts +36 -0
@@ -0,0 +1,53 @@
1
+ import React from 'react';
2
+
3
+ interface XerticaXLogoProps {
4
+ className?: string;
5
+ color?: string;
6
+ variant?: 'default' | 'white' | 'theme';
7
+ }
8
+
9
+ export function XerticaXLogo({
10
+ className = "w-auto h-8",
11
+ color,
12
+ variant = 'default'
13
+ }: XerticaXLogoProps) {
14
+
15
+ // Determinar a cor baseada na variante
16
+ const getColor = () => {
17
+ if (color) return color;
18
+
19
+ switch (variant) {
20
+ case 'white':
21
+ return '#FFFFFF';
22
+ case 'theme':
23
+ return 'currentColor';
24
+ default:
25
+ return 'var(--primary)';
26
+ }
27
+ };
28
+
29
+ return (
30
+ <svg
31
+ className={className}
32
+ fill="none"
33
+ preserveAspectRatio="xMidYMid meet"
34
+ viewBox="0 0 258 282"
35
+ xmlns="http://www.w3.org/2000/svg"
36
+ >
37
+ <g id="Xertica X">
38
+ <path
39
+ d="M67.2666 0.0361328L89.7793 33.3447L159.63 137.228L84.7471 248.178L61.8115 282H0L97.7109 137.228L5.45508 0.0361328H67.2666Z"
40
+ fill={getColor()}
41
+ />
42
+ <path
43
+ d="M258 282H196.185L154.114 219.951L185.192 174.123L258 282Z"
44
+ fill={getColor()}
45
+ />
46
+ <path
47
+ d="M252.696 0.186523L185.042 100.142L154.106 54.0693L190.884 0L252.696 0.186523Z"
48
+ fill={getColor()}
49
+ />
50
+ </g>
51
+ </svg>
52
+ );
53
+ }
@@ -0,0 +1,530 @@
1
+ import React, { useState, useRef, useEffect, useCallback } from 'react';
2
+ import { Map } from '../ui/map';
3
+ import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '../ui/card';
4
+ import { Button } from '../ui/button';
5
+ import { Badge } from '../ui/badge';
6
+ import { Separator } from '../ui/separator';
7
+ import {
8
+ MousePointer2,
9
+ MapPin,
10
+ Circle,
11
+ Square,
12
+ Hexagon,
13
+ Trash2,
14
+ Save,
15
+ Undo,
16
+ Check,
17
+ X
18
+ } from 'lucide-react';
19
+ import { toast } from 'sonner';
20
+ import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription } from '../ui/dialog';
21
+ import { ScrollArea } from '../ui/scroll-area';
22
+
23
+ // Tipos para os modos de desenho
24
+ type DrawingMode = 'marker' | 'circle' | 'rectangle' | 'polygon' | null;
25
+
26
+ // Tipo para objetos desenhados
27
+ type DrawnShape = {
28
+ type: DrawingMode;
29
+ overlay: google.maps.marker.AdvancedMarkerElement | google.maps.Circle | google.maps.Rectangle | google.maps.Polygon;
30
+ data?: any;
31
+ };
32
+
33
+ export function DrawingMapExample() {
34
+ const [mapInstance, setMapInstance] = useState<google.maps.Map | null>(null);
35
+ const [selectedMode, setSelectedMode] = useState<DrawingMode>(null);
36
+ const [shapes, setShapes] = useState<DrawnShape[]>([]);
37
+ const [showSaveDialog, setShowSaveDialog] = useState(false);
38
+ const [savedData, setSavedData] = useState('');
39
+
40
+ // Estado para desenho de polígono em andamento
41
+ const [isDrawingPolygon, setIsDrawingPolygon] = useState(false);
42
+ const tempPolylineRef = useRef<google.maps.Polyline | null>(null);
43
+ const polygonPathRef = useRef<google.maps.LatLng[]>([]);
44
+
45
+ // Refs para listeners do mapa para limpeza adequada
46
+ const mapListenersRef = useRef<google.maps.MapsEventListener[]>([]);
47
+
48
+ // Cores do Design System (Xertica Primary)
49
+ const colors = {
50
+ fill: '#2C275B', // Primary do CSS
51
+ stroke: '#2C275B',
52
+ fillOpacity: 0.2,
53
+ strokeWeight: 2,
54
+ editable: true,
55
+ draggable: true
56
+ };
57
+
58
+ // --- Funções de Desenho (definidas com useCallback para dependências) ---
59
+
60
+ const addMarker = useCallback((position: google.maps.LatLng) => {
61
+ if (!mapInstance) return;
62
+
63
+ // Utilizando AdvancedMarkerElement conforme recomendação (Marker depreciado)
64
+ const marker = new google.maps.marker.AdvancedMarkerElement({
65
+ position,
66
+ map: mapInstance,
67
+ gmpDraggable: true, // Propriedade correta para AdvancedMarkerElement
68
+ title: "Marcador"
69
+ });
70
+
71
+ marker.addListener('dragend', () => {
72
+ toast.info('Posição do marcador atualizada');
73
+ });
74
+
75
+ const newShape: DrawnShape = { type: 'marker', overlay: marker };
76
+ setShapes(prev => [...prev, newShape]);
77
+ toast.success('Marcador adicionado');
78
+ }, [mapInstance]);
79
+
80
+ const addCircle = useCallback((center: google.maps.LatLng) => {
81
+ if (!mapInstance) return;
82
+
83
+ const circle = new google.maps.Circle({
84
+ map: mapInstance,
85
+ center: center,
86
+ radius: 500,
87
+ fillColor: colors.fill,
88
+ fillOpacity: colors.fillOpacity,
89
+ strokeColor: colors.stroke,
90
+ strokeWeight: colors.strokeWeight,
91
+ editable: true,
92
+ draggable: true,
93
+ });
94
+
95
+ const newShape: DrawnShape = { type: 'circle', overlay: circle };
96
+ setShapes(prev => [...prev, newShape]);
97
+ toast.success('Círculo adicionado');
98
+ }, [mapInstance, colors]);
99
+
100
+ const addRectangle = useCallback((center: google.maps.LatLng) => {
101
+ if (!mapInstance) return;
102
+
103
+ const offset = 0.005;
104
+ const bounds = {
105
+ north: center.lat() + offset,
106
+ south: center.lat() - offset,
107
+ east: center.lng() + offset,
108
+ west: center.lng() - offset,
109
+ };
110
+
111
+ const rectangle = new google.maps.Rectangle({
112
+ map: mapInstance,
113
+ bounds: bounds,
114
+ fillColor: colors.fill,
115
+ fillOpacity: colors.fillOpacity,
116
+ strokeColor: colors.stroke,
117
+ strokeWeight: colors.strokeWeight,
118
+ editable: true,
119
+ draggable: true,
120
+ });
121
+
122
+ const newShape: DrawnShape = { type: 'rectangle', overlay: rectangle };
123
+ setShapes(prev => [...prev, newShape]);
124
+ toast.success('Retângulo adicionado');
125
+ }, [mapInstance, colors]);
126
+
127
+ const cancelPolygonDrawing = useCallback(() => {
128
+ if (tempPolylineRef.current) {
129
+ tempPolylineRef.current.setMap(null);
130
+ tempPolylineRef.current = null;
131
+ }
132
+ polygonPathRef.current = [];
133
+ setIsDrawingPolygon(false);
134
+ }, []);
135
+
136
+ const finishPolygon = useCallback(() => {
137
+ if (!mapInstance || polygonPathRef.current.length < 3) {
138
+ if (polygonPathRef.current.length < 3 && polygonPathRef.current.length > 0) {
139
+ toast.error('Polígono precisa de pelo menos 3 pontos.');
140
+ }
141
+ return;
142
+ }
143
+
144
+ const polygon = new google.maps.Polygon({
145
+ map: mapInstance,
146
+ paths: polygonPathRef.current,
147
+ fillColor: colors.fill,
148
+ fillOpacity: colors.fillOpacity,
149
+ strokeColor: colors.stroke,
150
+ strokeWeight: colors.strokeWeight,
151
+ editable: true,
152
+ draggable: true,
153
+ });
154
+
155
+ const newShape: DrawnShape = { type: 'polygon', overlay: polygon };
156
+ setShapes(prev => [...prev, newShape]);
157
+
158
+ cancelPolygonDrawing();
159
+ toast.success('Polígono criado com sucesso!');
160
+ }, [mapInstance, colors, cancelPolygonDrawing]);
161
+
162
+ const addPolygonPoint = useCallback((point: google.maps.LatLng) => {
163
+ if (!mapInstance) return;
164
+
165
+ if (!isDrawingPolygon) {
166
+ setIsDrawingPolygon(true);
167
+ polygonPathRef.current = [point];
168
+
169
+ tempPolylineRef.current = new google.maps.Polyline({
170
+ map: mapInstance,
171
+ path: polygonPathRef.current,
172
+ strokeColor: colors.stroke,
173
+ strokeOpacity: 0.8,
174
+ strokeWeight: 2,
175
+ geodesic: true,
176
+ clickable: false // Importante: não capturar cliques para não atrapalhar o mapa
177
+ });
178
+
179
+ toast.info('Clique para adicionar pontos. Duplo clique ou botão check para fechar.', { duration: 4000 });
180
+ } else {
181
+ polygonPathRef.current.push(point);
182
+ tempPolylineRef.current?.setPath(polygonPathRef.current);
183
+ }
184
+ }, [mapInstance, isDrawingPolygon, colors]);
185
+
186
+ // --- Setup dos Listeners ---
187
+
188
+ // Limpar listeners antigos
189
+ const clearListeners = () => {
190
+ mapListenersRef.current.forEach(listener => google.maps.event.removeListener(listener));
191
+ mapListenersRef.current = [];
192
+ };
193
+
194
+ useEffect(() => {
195
+ if (!mapInstance) return;
196
+
197
+ clearListeners();
198
+
199
+ if (!selectedMode) {
200
+ mapInstance.setOptions({ draggableCursor: 'grab', clickableIcons: true });
201
+ return;
202
+ }
203
+
204
+ mapInstance.setOptions({
205
+ draggableCursor: 'crosshair',
206
+ clickableIcons: false
207
+ });
208
+
209
+ const clickListener = mapInstance.addListener('click', (e: google.maps.MapMouseEvent) => {
210
+ if (!e.latLng) return;
211
+
212
+ switch (selectedMode) {
213
+ case 'marker':
214
+ addMarker(e.latLng);
215
+ break;
216
+ case 'circle':
217
+ addCircle(e.latLng);
218
+ break;
219
+ case 'rectangle':
220
+ addRectangle(e.latLng);
221
+ break;
222
+ case 'polygon':
223
+ addPolygonPoint(e.latLng);
224
+ break;
225
+ }
226
+ });
227
+
228
+ mapListenersRef.current.push(clickListener);
229
+
230
+ // Listener especial para fechar polígono com duplo clique
231
+ if (selectedMode === 'polygon') {
232
+ const dblClickListener = mapInstance.addListener('dblclick', (e: any) => {
233
+ if (e.domEvent) e.domEvent.stopPropagation();
234
+ finishPolygon();
235
+ });
236
+ mapListenersRef.current.push(dblClickListener);
237
+ }
238
+
239
+ return () => {
240
+ clearListeners();
241
+ };
242
+ }, [mapInstance, selectedMode, addMarker, addCircle, addRectangle, addPolygonPoint, finishPolygon]);
243
+
244
+
245
+ // --- Ações Gerais ---
246
+
247
+ const handleModeChange = (mode: DrawingMode) => {
248
+ if (isDrawingPolygon) {
249
+ cancelPolygonDrawing();
250
+ }
251
+ setSelectedMode(mode);
252
+ };
253
+
254
+ const clearAll = () => {
255
+ shapes.forEach(shape => shape.overlay.setMap(null));
256
+ setShapes([]);
257
+ if (isDrawingPolygon) cancelPolygonDrawing();
258
+ toast.info('Mapa limpo');
259
+ };
260
+
261
+ const undoLast = () => {
262
+ if (isDrawingPolygon) {
263
+ if (polygonPathRef.current.length > 0) {
264
+ polygonPathRef.current.pop();
265
+ tempPolylineRef.current?.setPath(polygonPathRef.current);
266
+ if (polygonPathRef.current.length === 0) {
267
+ cancelPolygonDrawing();
268
+ }
269
+ }
270
+ return;
271
+ }
272
+
273
+ if (shapes.length === 0) return;
274
+
275
+ const lastShape = shapes[shapes.length - 1];
276
+ lastShape.overlay.setMap(null);
277
+ setShapes(prev => prev.slice(0, prev.length - 1));
278
+ };
279
+
280
+ const handleSave = () => {
281
+ if (shapes.length === 0) {
282
+ toast.error('Adicione desenhos ao mapa antes de salvar.');
283
+ return;
284
+ }
285
+
286
+ const exportData = shapes.map(s => {
287
+ let data: any = {};
288
+
289
+ if (s.type === 'marker') {
290
+ const marker = s.overlay as google.maps.marker.AdvancedMarkerElement;
291
+ const pos = marker.position;
292
+ // Tratamento seguro para lat/lng que pode ser LatLng object ou LatLngLiteral
293
+ if (pos) {
294
+ const lat = typeof (pos as any).lat === 'function' ? (pos as any).lat() : (pos as any).lat;
295
+ const lng = typeof (pos as any).lng === 'function' ? (pos as any).lng() : (pos as any).lng;
296
+ data = { lat, lng };
297
+ }
298
+ }
299
+ else if (s.type === 'circle') {
300
+ const circle = s.overlay as google.maps.Circle;
301
+ data = {
302
+ center: { lat: circle.getCenter()?.lat(), lng: circle.getCenter()?.lng() },
303
+ radius: circle.getRadius()
304
+ };
305
+ }
306
+ else if (s.type === 'rectangle') {
307
+ const rect = s.overlay as google.maps.Rectangle;
308
+ const bounds = rect.getBounds();
309
+ data = {
310
+ north: bounds?.getNorthEast().lat(),
311
+ south: bounds?.getSouthWest().lat(),
312
+ east: bounds?.getNorthEast().lng(),
313
+ west: bounds?.getSouthWest().lng()
314
+ };
315
+ }
316
+ else if (s.type === 'polygon') {
317
+ const poly = s.overlay as google.maps.Polygon;
318
+ const path = poly.getPath();
319
+ data = path.getArray().map(coord => ({ lat: coord.lat(), lng: coord.lng() }));
320
+ }
321
+
322
+ return { type: s.type, data };
323
+ });
324
+
325
+ setSavedData(JSON.stringify(exportData, null, 2));
326
+ setShowSaveDialog(true);
327
+ };
328
+
329
+ // Botão auxiliar para renderizar itens da toolbar
330
+ const ToolButton = ({
331
+ active,
332
+ onClick,
333
+ icon: Icon,
334
+ label
335
+ }: {
336
+ active?: boolean;
337
+ onClick: () => void;
338
+ icon: any;
339
+ label: string
340
+ }) => (
341
+ <Button
342
+ variant={active ? "default" : "ghost"}
343
+ className={`w-full justify-start gap-3 ${active ? 'bg-primary text-primary-foreground' : 'text-muted-foreground hover:text-foreground'}`}
344
+ onClick={onClick}
345
+ >
346
+ <Icon className="h-4 w-4" />
347
+ <span className="text-sm font-medium">{label}</span>
348
+ </Button>
349
+ );
350
+
351
+ return (
352
+ <Card className="h-full border-0 shadow-none md:border md:shadow-sm">
353
+ <CardHeader className="border-b bg-muted/20 pb-4">
354
+ <div className="flex items-center justify-between">
355
+ <div>
356
+ <CardTitle>Ferramentas de Desenho</CardTitle>
357
+ <CardDescription className="mt-1">
358
+ Crie geometrias personalizadas no mapa
359
+ </CardDescription>
360
+ </div>
361
+ <Badge variant="outline" className="h-6">
362
+ {shapes.length} {shapes.length === 1 ? 'elemento' : 'elementos'}
363
+ </Badge>
364
+ </div>
365
+ </CardHeader>
366
+
367
+ <CardContent className="p-0 flex flex-col md:flex-row h-[600px] bg-background">
368
+ {/* Sidebar de Ferramentas */}
369
+ <div className="w-full md:w-64 border-b md:border-b-0 md:border-r bg-muted/10 flex flex-col">
370
+ <ScrollArea className="flex-1">
371
+ <div className="p-4 space-y-6">
372
+ {/* Seção Modos */}
373
+ <div className="space-y-3">
374
+ <h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider px-2">
375
+ Ferramentas
376
+ </h4>
377
+ <div className="space-y-1">
378
+ <ToolButton
379
+ active={selectedMode === null}
380
+ onClick={() => handleModeChange(null)}
381
+ icon={MousePointer2}
382
+ label="Navegar"
383
+ />
384
+ <ToolButton
385
+ active={selectedMode === 'marker'}
386
+ onClick={() => handleModeChange('marker')}
387
+ icon={MapPin}
388
+ label="Marcador"
389
+ />
390
+ <ToolButton
391
+ active={selectedMode === 'circle'}
392
+ onClick={() => handleModeChange('circle')}
393
+ icon={Circle}
394
+ label="Círculo"
395
+ />
396
+ <ToolButton
397
+ active={selectedMode === 'rectangle'}
398
+ onClick={() => handleModeChange('rectangle')}
399
+ icon={Square}
400
+ label="Retângulo"
401
+ />
402
+ <ToolButton
403
+ active={selectedMode === 'polygon'}
404
+ onClick={() => handleModeChange('polygon')}
405
+ icon={Hexagon}
406
+ label="Polígono"
407
+ />
408
+ </div>
409
+ </div>
410
+
411
+ <Separator />
412
+
413
+ {/* Seção Ações */}
414
+ <div className="space-y-3">
415
+ <h4 className="text-xs font-semibold text-muted-foreground uppercase tracking-wider px-2">
416
+ Ações
417
+ </h4>
418
+ <div className="grid grid-cols-2 gap-2">
419
+ <Button
420
+ variant="outline"
421
+ onClick={undoLast}
422
+ disabled={shapes.length === 0 && !isDrawingPolygon}
423
+ className="w-full"
424
+ title="Desfazer último"
425
+ >
426
+ <Undo className="h-4 w-4 mr-2" />
427
+ Desfazer
428
+ </Button>
429
+ <Button
430
+ variant="destructive"
431
+ onClick={clearAll}
432
+ disabled={shapes.length === 0 && !isDrawingPolygon}
433
+ className="w-full bg-destructive/10 text-destructive hover:bg-destructive hover:text-destructive-foreground border-0 shadow-none"
434
+ >
435
+ <Trash2 className="h-4 w-4 mr-2" />
436
+ Limpar
437
+ </Button>
438
+ </div>
439
+
440
+ <Button
441
+ variant="default"
442
+ onClick={handleSave}
443
+ disabled={shapes.length === 0}
444
+ className="w-full mt-2"
445
+ >
446
+ <Save className="h-4 w-4 mr-2" />
447
+ Salvar GeoJSON
448
+ </Button>
449
+ </div>
450
+ </div>
451
+ </ScrollArea>
452
+
453
+ {/* Status do Polígono (Floating Action na Sidebar em mobile, ou fixo embaixo em desktop) */}
454
+ {isDrawingPolygon && (
455
+ <div className="p-4 bg-primary/5 border-t border-primary/10">
456
+ <div className="flex items-center gap-2 mb-2 text-primary text-xs font-medium">
457
+ <span className="relative flex h-2 w-2">
458
+ <span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-primary opacity-75"></span>
459
+ <span className="relative inline-flex rounded-full h-2 w-2 bg-primary"></span>
460
+ </span>
461
+ Desenhando Polígono...
462
+ </div>
463
+ <div className="flex gap-2">
464
+ <Button
465
+ size="sm"
466
+ className="w-full bg-green-600 hover:bg-green-700 text-white"
467
+ onClick={finishPolygon}
468
+ >
469
+ <Check className="h-3 w-3 mr-1" /> Concluir
470
+ </Button>
471
+ <Button
472
+ size="sm"
473
+ variant="outline"
474
+ className="w-full"
475
+ onClick={cancelPolygonDrawing}
476
+ >
477
+ <X className="h-3 w-3 mr-1" /> Cancelar
478
+ </Button>
479
+ </div>
480
+ </div>
481
+ )}
482
+ </div>
483
+
484
+ {/* Área do Mapa */}
485
+ <div className="flex-1 relative bg-muted/5 min-h-[400px]">
486
+ <Map
487
+ className="h-full w-full rounded-none md:rounded-br-lg"
488
+ center={{ lat: -23.5505, lng: -46.6333 }}
489
+ zoom={13}
490
+ height="100%"
491
+ mapContainerClassName="h-full w-full rounded-none md:rounded-br-lg"
492
+ disableDefaultUI={true}
493
+ zoomControl={true}
494
+ onMapLoad={setMapInstance}
495
+ />
496
+
497
+ {/* Instruções Flutuantes */}
498
+ {selectedMode && !isDrawingPolygon && (
499
+ <div className="absolute bottom-6 left-1/2 -translate-x-1/2 bg-background/90 backdrop-blur border px-4 py-2 rounded-full shadow-sm text-xs font-medium text-muted-foreground pointer-events-none z-10">
500
+ {selectedMode === 'marker' && 'Clique no mapa para adicionar um marcador'}
501
+ {selectedMode === 'circle' && 'Clique no mapa para definir o centro do círculo'}
502
+ {selectedMode === 'rectangle' && 'Clique no mapa para criar um retângulo'}
503
+ {selectedMode === 'polygon' && 'Clique sequencialmente para desenhar a área'}
504
+ </div>
505
+ )}
506
+ </div>
507
+
508
+ {/* Dialog de Salvamento */}
509
+ <Dialog open={showSaveDialog} onOpenChange={setShowSaveDialog}>
510
+ <DialogContent className="max-w-md">
511
+ <DialogHeader>
512
+ <DialogTitle>Dados Geográficos (GeoJSON)</DialogTitle>
513
+ <DialogDescription>
514
+ Copie os dados das formas desenhadas abaixo.
515
+ </DialogDescription>
516
+ </DialogHeader>
517
+ <div className="relative mt-2">
518
+ <pre className="p-4 rounded-lg bg-muted overflow-auto max-h-[300px] text-xs font-mono" data-custom-scrollbar>
519
+ {savedData}
520
+ </pre>
521
+ </div>
522
+ <div className="flex justify-end mt-4">
523
+ <Button onClick={() => setShowSaveDialog(false)}>Fechar</Button>
524
+ </div>
525
+ </DialogContent>
526
+ </Dialog>
527
+ </CardContent>
528
+ </Card>
529
+ );
530
+ }