xertica-ui 1.0.0 → 1.1.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/bin/cli.ts +81 -0
- package/components/ui/google-maps-loader.tsx +31 -26
- package/components/ui/map.tsx +11 -11
- package/components/ui/route-map.tsx +13 -13
- package/components/ui/xertica-assistant.tsx +127 -124
- package/dist/cli.js +103 -20812
- package/guidelines/Guidelines.md +643 -46
- package/package.json +2 -2
package/bin/cli.ts
CHANGED
|
@@ -96,6 +96,11 @@ program
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
// Create .env.example
|
|
100
|
+
const envExampleContent = `VITE_GOOGLE_MAPS_API_KEY=
|
|
101
|
+
VITE_GEMINI_API_KEY=`;
|
|
102
|
+
await fs.writeFile(path.join(targetDir, '.env.example'), envExampleContent);
|
|
103
|
+
|
|
99
104
|
// Handle App.tsx specifically
|
|
100
105
|
const pages = response.pages || [];
|
|
101
106
|
const hasLogin = pages.includes('login');
|
|
@@ -190,4 +195,80 @@ export default function App() {
|
|
|
190
195
|
}
|
|
191
196
|
});
|
|
192
197
|
|
|
198
|
+
program
|
|
199
|
+
.command('update')
|
|
200
|
+
.description('Update components in your project')
|
|
201
|
+
.argument('[components...]', 'Components to update')
|
|
202
|
+
.option('-a, --all', 'Update all installed components')
|
|
203
|
+
.action(async (components, options) => {
|
|
204
|
+
const targetDir = process.cwd();
|
|
205
|
+
const sourceRoot = path.resolve(__dirname, '../');
|
|
206
|
+
const uiComponentsDir = path.join(targetDir, 'components/ui');
|
|
207
|
+
const sourceUiDir = path.join(sourceRoot, 'components/ui');
|
|
208
|
+
|
|
209
|
+
if (!await fs.pathExists(uiComponentsDir)) {
|
|
210
|
+
console.error(chalk.red('Components directory not found. Is this a Xertica UI project?'));
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (!await fs.pathExists(sourceUiDir)) {
|
|
215
|
+
console.error(chalk.red('Source components not found. Try reinstalling the CLI package.'));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
let componentsToUpdate: string[] = [];
|
|
220
|
+
|
|
221
|
+
if (options.all) {
|
|
222
|
+
const files = await fs.readdir(uiComponentsDir);
|
|
223
|
+
componentsToUpdate = files.filter(f => f.endsWith('.tsx'));
|
|
224
|
+
} else if (components && components.length > 0) {
|
|
225
|
+
componentsToUpdate = components.map((c: string) => c.endsWith('.tsx') ? c : `${c}.tsx`);
|
|
226
|
+
} else {
|
|
227
|
+
// Interactive mode
|
|
228
|
+
const files = await fs.readdir(uiComponentsDir);
|
|
229
|
+
const installedComponents = files.filter(f => f.endsWith('.tsx'));
|
|
230
|
+
|
|
231
|
+
if (installedComponents.length === 0) {
|
|
232
|
+
console.log(chalk.yellow('No components found to update.'));
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const response = await prompts({
|
|
237
|
+
type: 'multiselect',
|
|
238
|
+
name: 'selected',
|
|
239
|
+
message: 'Select components to update',
|
|
240
|
+
choices: installedComponents.map(f => ({ title: f, value: f })),
|
|
241
|
+
min: 1
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
if (!response.selected) return;
|
|
245
|
+
componentsToUpdate = response.selected;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const spinner = ora('Updating components...').start();
|
|
249
|
+
|
|
250
|
+
try {
|
|
251
|
+
let updatedCount = 0;
|
|
252
|
+
for (const component of componentsToUpdate) {
|
|
253
|
+
const srcPath = path.join(sourceUiDir, component);
|
|
254
|
+
const destPath = path.join(uiComponentsDir, component);
|
|
255
|
+
|
|
256
|
+
if (await fs.pathExists(srcPath)) {
|
|
257
|
+
await fs.copy(srcPath, destPath);
|
|
258
|
+
updatedCount++;
|
|
259
|
+
} else {
|
|
260
|
+
spinner.warn(`Component ${component} not found in source.`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
if (updatedCount > 0) {
|
|
264
|
+
spinner.succeed(`Successfully updated ${updatedCount} components!`);
|
|
265
|
+
} else {
|
|
266
|
+
spinner.info('No components updated.');
|
|
267
|
+
}
|
|
268
|
+
} catch (error) {
|
|
269
|
+
spinner.fail('Failed to update components');
|
|
270
|
+
console.error(error);
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
|
|
193
274
|
program.parse();
|
|
@@ -29,14 +29,19 @@ declare global {
|
|
|
29
29
|
* Função auxiliar para ler a API key do localStorage de forma síncrona
|
|
30
30
|
*/
|
|
31
31
|
function getInitialApiKey(): string | undefined {
|
|
32
|
+
// 1. Check Environment Variable (Vite)
|
|
33
|
+
if (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_GOOGLE_MAPS_API_KEY) {
|
|
34
|
+
return import.meta.env.VITE_GOOGLE_MAPS_API_KEY as string;
|
|
35
|
+
}
|
|
36
|
+
|
|
32
37
|
if (typeof window === 'undefined') return undefined;
|
|
33
|
-
|
|
38
|
+
|
|
34
39
|
const savedKey = localStorage.getItem('xertica-googlemaps-api-key');
|
|
35
|
-
|
|
40
|
+
|
|
36
41
|
if (savedKey && savedKey.trim().length > 0) {
|
|
37
42
|
return savedKey;
|
|
38
43
|
}
|
|
39
|
-
|
|
44
|
+
|
|
40
45
|
return undefined;
|
|
41
46
|
}
|
|
42
47
|
|
|
@@ -45,23 +50,23 @@ function getInitialApiKey(): string | undefined {
|
|
|
45
50
|
*/
|
|
46
51
|
function removeExistingScript(): void {
|
|
47
52
|
if (typeof window === 'undefined') return;
|
|
48
|
-
|
|
53
|
+
|
|
49
54
|
// Remover callback global se existir
|
|
50
55
|
if ((window as any).__googleMapsCallback) {
|
|
51
56
|
delete (window as any).__googleMapsCallback;
|
|
52
57
|
}
|
|
53
|
-
|
|
58
|
+
|
|
54
59
|
// Remover script existente
|
|
55
60
|
const existingScript = document.querySelector(`script[src*=\"maps.googleapis.com/maps/api/js\"]`);
|
|
56
61
|
if (existingScript) {
|
|
57
62
|
existingScript.remove();
|
|
58
63
|
}
|
|
59
|
-
|
|
64
|
+
|
|
60
65
|
// Limpar Google Maps do window
|
|
61
66
|
if ((window as any).google?.maps) {
|
|
62
67
|
delete (window as any).google.maps;
|
|
63
68
|
}
|
|
64
|
-
|
|
69
|
+
|
|
65
70
|
// Limpar singleton para permitir novo carregamento
|
|
66
71
|
if (window.__XERTICA_GOOGLE_MAPS_LOADER__) {
|
|
67
72
|
window.__XERTICA_GOOGLE_MAPS_LOADER__.isLoaded = false;
|
|
@@ -121,7 +126,7 @@ function loadGoogleMapsScript(apiKey?: string): Promise<void> {
|
|
|
121
126
|
reject(new Error('API key changed. Please reload the page to apply changes.'));
|
|
122
127
|
return;
|
|
123
128
|
}
|
|
124
|
-
|
|
129
|
+
|
|
125
130
|
// Aguardar o script carregar
|
|
126
131
|
if (isGoogleMapsAlreadyLoaded() && isMarkerLibraryLoaded()) {
|
|
127
132
|
resolve();
|
|
@@ -175,7 +180,7 @@ function loadGoogleMapsScript(apiKey?: string): Promise<void> {
|
|
|
175
180
|
});
|
|
176
181
|
|
|
177
182
|
document.head.appendChild(script);
|
|
178
|
-
|
|
183
|
+
|
|
179
184
|
// Salvar referência ao script
|
|
180
185
|
const singleton = getOrCreateSingleton();
|
|
181
186
|
if (singleton) {
|
|
@@ -189,17 +194,17 @@ function loadGoogleMapsScript(apiKey?: string): Promise<void> {
|
|
|
189
194
|
*/
|
|
190
195
|
function getOrCreateSingleton() {
|
|
191
196
|
if (typeof window === 'undefined') return null;
|
|
192
|
-
|
|
197
|
+
|
|
193
198
|
if (!window.__XERTICA_GOOGLE_MAPS_LOADER__) {
|
|
194
199
|
const isPreloaded = isGoogleMapsAlreadyLoaded();
|
|
195
|
-
|
|
200
|
+
|
|
196
201
|
window.__XERTICA_GOOGLE_MAPS_LOADER__ = {
|
|
197
202
|
isLoaded: isPreloaded,
|
|
198
203
|
loadError: undefined,
|
|
199
204
|
listeners: new Set(),
|
|
200
205
|
};
|
|
201
206
|
}
|
|
202
|
-
|
|
207
|
+
|
|
203
208
|
return window.__XERTICA_GOOGLE_MAPS_LOADER__;
|
|
204
209
|
}
|
|
205
210
|
|
|
@@ -209,10 +214,10 @@ function getOrCreateSingleton() {
|
|
|
209
214
|
function updateSingleton(state: Partial<GoogleMapsContextType>) {
|
|
210
215
|
const singleton = getOrCreateSingleton();
|
|
211
216
|
if (!singleton) return;
|
|
212
|
-
|
|
217
|
+
|
|
213
218
|
if (state.isLoaded !== undefined) singleton.isLoaded = state.isLoaded;
|
|
214
219
|
if (state.loadError !== undefined) singleton.loadError = state.loadError;
|
|
215
|
-
|
|
220
|
+
|
|
216
221
|
const newState = { isLoaded: singleton.isLoaded, loadError: singleton.loadError };
|
|
217
222
|
singleton.listeners.forEach(listener => listener(newState));
|
|
218
223
|
}
|
|
@@ -224,26 +229,26 @@ const SingletonLoaderWrapper = ({ children }: { children: ReactNode }) => {
|
|
|
224
229
|
const [state, setState] = useState<GoogleMapsContextType>(() => {
|
|
225
230
|
const singleton = getOrCreateSingleton();
|
|
226
231
|
if (!singleton) return { isLoaded: false, loadError: undefined };
|
|
227
|
-
|
|
232
|
+
|
|
228
233
|
return {
|
|
229
234
|
isLoaded: singleton.isLoaded,
|
|
230
235
|
loadError: singleton.loadError,
|
|
231
236
|
};
|
|
232
237
|
});
|
|
233
|
-
|
|
238
|
+
|
|
234
239
|
useEffect(() => {
|
|
235
240
|
const singleton = getOrCreateSingleton();
|
|
236
241
|
if (!singleton) return;
|
|
237
|
-
|
|
242
|
+
|
|
238
243
|
const listener = (newState: GoogleMapsContextType) => {
|
|
239
244
|
setState(newState);
|
|
240
245
|
};
|
|
241
|
-
|
|
246
|
+
|
|
242
247
|
singleton.listeners.add(listener);
|
|
243
|
-
|
|
248
|
+
|
|
244
249
|
// Sincronizar estado inicial
|
|
245
250
|
listener({ isLoaded: singleton.isLoaded, loadError: singleton.loadError });
|
|
246
|
-
|
|
251
|
+
|
|
247
252
|
return () => {
|
|
248
253
|
singleton.listeners.delete(listener);
|
|
249
254
|
};
|
|
@@ -281,16 +286,16 @@ const LoaderInitializer = () => {
|
|
|
281
286
|
hasInitializedRef.current = true;
|
|
282
287
|
|
|
283
288
|
const apiKey = getInitialApiKey();
|
|
284
|
-
|
|
289
|
+
|
|
285
290
|
// Se não houver API key, apenas marcar como não carregado (sem erro)
|
|
286
291
|
if (!apiKey) {
|
|
287
|
-
updateSingleton({
|
|
288
|
-
isLoaded: false,
|
|
292
|
+
updateSingleton({
|
|
293
|
+
isLoaded: false,
|
|
289
294
|
loadError: undefined // Não definir erro quando não há API key
|
|
290
295
|
});
|
|
291
296
|
return;
|
|
292
297
|
}
|
|
293
|
-
|
|
298
|
+
|
|
294
299
|
loadGoogleMapsScript(apiKey)
|
|
295
300
|
.then(() => {
|
|
296
301
|
updateSingleton({ isLoaded: true, loadError: undefined });
|
|
@@ -313,13 +318,13 @@ export const GoogleMapsLoaderProvider = ({ children }: { children: ReactNode })
|
|
|
313
318
|
const [shouldInitialize] = useState(() => {
|
|
314
319
|
const singleton = getOrCreateSingleton();
|
|
315
320
|
if (!singleton) return false;
|
|
316
|
-
|
|
321
|
+
|
|
317
322
|
// Se já está carregado, não inicializar
|
|
318
323
|
if (singleton.isLoaded || isGoogleMapsAlreadyLoaded() || isMarkerLibraryLoaded()) {
|
|
319
324
|
singleton.isLoaded = true;
|
|
320
325
|
return false;
|
|
321
326
|
}
|
|
322
|
-
|
|
327
|
+
|
|
323
328
|
return true;
|
|
324
329
|
});
|
|
325
330
|
|
package/components/ui/map.tsx
CHANGED
|
@@ -112,7 +112,7 @@ const MapContent = React.forwardRef<HTMLDivElement, MapProps & { apiKey: string
|
|
|
112
112
|
});
|
|
113
113
|
|
|
114
114
|
mapRef.current = map;
|
|
115
|
-
|
|
115
|
+
|
|
116
116
|
if (onMapLoad) {
|
|
117
117
|
onMapLoad(map);
|
|
118
118
|
}
|
|
@@ -147,7 +147,7 @@ const MapContent = React.forwardRef<HTMLDivElement, MapProps & { apiKey: string
|
|
|
147
147
|
|
|
148
148
|
const iconContainer = document.createElement('div');
|
|
149
149
|
iconContainer.className = 'flex items-center justify-center rotate-45';
|
|
150
|
-
|
|
150
|
+
|
|
151
151
|
if (markerData.iconSvg) {
|
|
152
152
|
const div = document.createElement('div');
|
|
153
153
|
div.innerHTML = markerData.iconSvg;
|
|
@@ -367,18 +367,18 @@ MapContent.displayName = "MapContent";
|
|
|
367
367
|
|
|
368
368
|
export const Map = React.forwardRef<HTMLDivElement, MapProps>(
|
|
369
369
|
(props, ref) => {
|
|
370
|
-
const effectiveApiKey = props.apiKey || "";
|
|
370
|
+
const effectiveApiKey = props.apiKey || (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_GOOGLE_MAPS_API_KEY) || "";
|
|
371
371
|
|
|
372
|
-
const isValidKey = effectiveApiKey &&
|
|
373
|
-
|
|
374
|
-
|
|
372
|
+
const isValidKey = effectiveApiKey &&
|
|
373
|
+
effectiveApiKey !== "YOUR_GOOGLE_MAPS_API_KEY_HERE" &&
|
|
374
|
+
effectiveApiKey.startsWith("AIza");
|
|
375
375
|
|
|
376
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
|
|
377
|
+
const {
|
|
378
|
+
center, zoom, markers, circle, polygon, height, apiKey,
|
|
379
|
+
mapContainerClassName, disableDefaultUI, zoomControl,
|
|
380
|
+
streetViewControl, mapTypeControl, fullscreenControl,
|
|
381
|
+
gestureHandling, layers, ...divProps
|
|
382
382
|
} = props;
|
|
383
383
|
|
|
384
384
|
return (
|
|
@@ -91,7 +91,7 @@ const RouteMapContent = React.forwardRef<HTMLDivElement, RouteMapProps & { apiKe
|
|
|
91
91
|
useEffect(() => {
|
|
92
92
|
const map = mapRef.current;
|
|
93
93
|
const renderer = directionsRendererRef.current;
|
|
94
|
-
|
|
94
|
+
|
|
95
95
|
if (!map || !renderer || !isLoaded || isCalculatingRef.current) return;
|
|
96
96
|
if (!origin || !destination) return;
|
|
97
97
|
|
|
@@ -111,28 +111,28 @@ const RouteMapContent = React.forwardRef<HTMLDivElement, RouteMapProps & { apiKe
|
|
|
111
111
|
|
|
112
112
|
directionsService.route(request, (result, status) => {
|
|
113
113
|
isCalculatingRef.current = false;
|
|
114
|
-
|
|
114
|
+
|
|
115
115
|
if (status === 'OK' && result) {
|
|
116
116
|
renderer.setDirections(result);
|
|
117
|
-
|
|
117
|
+
|
|
118
118
|
// Calculate total distance and duration
|
|
119
119
|
const route = result.routes[0];
|
|
120
120
|
if (route?.legs?.length > 0 && onRouteCalculated) {
|
|
121
121
|
let totalDistance = 0;
|
|
122
122
|
let totalDuration = 0;
|
|
123
|
-
|
|
123
|
+
|
|
124
124
|
route.legs.forEach(leg => {
|
|
125
125
|
if (leg.distance) totalDistance += leg.distance.value;
|
|
126
126
|
if (leg.duration) totalDuration += leg.duration.value;
|
|
127
127
|
});
|
|
128
|
-
|
|
128
|
+
|
|
129
129
|
const distanceKm = (totalDistance / 1000).toFixed(1);
|
|
130
130
|
const distanceText = `${distanceKm} km`;
|
|
131
|
-
|
|
131
|
+
|
|
132
132
|
const hours = Math.floor(totalDuration / 3600);
|
|
133
133
|
const minutes = Math.floor((totalDuration % 3600) / 60);
|
|
134
134
|
const durationText = hours > 0 ? `${hours}h ${minutes}min` : `${minutes} min`;
|
|
135
|
-
|
|
135
|
+
|
|
136
136
|
onRouteCalculated(distanceText, durationText);
|
|
137
137
|
}
|
|
138
138
|
}
|
|
@@ -201,18 +201,18 @@ RouteMapContent.displayName = "RouteMapContent";
|
|
|
201
201
|
|
|
202
202
|
export const RouteMap = React.forwardRef<HTMLDivElement, RouteMapProps>(
|
|
203
203
|
(props, ref) => {
|
|
204
|
-
const effectiveApiKey = props.apiKey || "";
|
|
204
|
+
const effectiveApiKey = props.apiKey || (typeof import.meta !== 'undefined' && import.meta.env && import.meta.env.VITE_GOOGLE_MAPS_API_KEY) || "";
|
|
205
205
|
|
|
206
|
-
const isValidKey = effectiveApiKey &&
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
const isValidKey = effectiveApiKey &&
|
|
207
|
+
effectiveApiKey !== "YOUR_GOOGLE_MAPS_API_KEY_HERE" &&
|
|
208
|
+
effectiveApiKey.startsWith("AIza");
|
|
209
209
|
|
|
210
210
|
if (!isValidKey) {
|
|
211
|
-
const {
|
|
211
|
+
const {
|
|
212
212
|
origin, destination, waypoints, travelMode, height, apiKey,
|
|
213
213
|
mapContainerClassName, disableDefaultUI, zoomControl,
|
|
214
214
|
streetViewControl, mapTypeControl, fullscreenControl,
|
|
215
|
-
onRouteCalculated, ...divProps
|
|
215
|
+
onRouteCalculated, ...divProps
|
|
216
216
|
} = props;
|
|
217
217
|
|
|
218
218
|
return (
|