xertica-ui 1.0.0 → 1.2.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 CHANGED
@@ -7,6 +7,7 @@ import fs from 'fs-extra';
7
7
  import path from 'path';
8
8
  import { fileURLToPath } from 'url';
9
9
  import { execa } from 'execa';
10
+ import { colorThemes } from '../contexts/theme-data';
10
11
 
11
12
  const __filename = fileURLToPath(import.meta.url);
12
13
  const __dirname = path.dirname(__filename);
@@ -34,7 +35,7 @@ program
34
35
  const files = await fs.readdir(uiComponentsDir);
35
36
  const components = files
36
37
  .filter(f => f.endsWith('.tsx') && !f.startsWith('index'))
37
- .map(f => ({ title: f.replace('.tsx', ''), value: f }));
38
+ .map(f => ({ title: f.replace('.tsx', ''), value: f, selected: true }));
38
39
 
39
40
  componentChoices = [
40
41
  { title: 'All Components', value: 'all', selected: true },
@@ -42,6 +43,8 @@ program
42
43
  ];
43
44
  }
44
45
 
46
+
47
+
45
48
  console.log(chalk.blue('🚀 Welcome to Xertica UI CLI!'));
46
49
 
47
50
  const response = await prompts([
@@ -49,7 +52,12 @@ program
49
52
  type: 'multiselect',
50
53
  name: 'components',
51
54
  message: 'Which components would you like to include?',
52
- choices: componentChoices,
55
+ choices: componentChoices.length > 1 ? componentChoices : [
56
+ { title: 'All Components', value: 'all', selected: true },
57
+ // Fetch components logic was above, I'll rely on existing code for that part
58
+ // and just insert my code around existing blocks in separate steps if needed.
59
+ // But replace_file_content requires me to match content.
60
+ ],
53
61
  hint: '- Space to select. Return to submit',
54
62
  min: 1
55
63
  },
@@ -63,6 +71,17 @@ program
63
71
  { title: 'Template Page', value: 'template', selected: true },
64
72
  ]
65
73
  },
74
+ {
75
+ type: 'select',
76
+ name: 'theme',
77
+ message: 'Select the default color theme for your project:',
78
+ choices: colorThemes.map(t => ({
79
+ title: t.name,
80
+ description: t.description,
81
+ value: t.id
82
+ })),
83
+ initial: 0
84
+ },
66
85
  {
67
86
  type: 'confirm',
68
87
  name: 'install',
@@ -96,6 +115,11 @@ program
96
115
  }
97
116
  }
98
117
 
118
+ // Create .env.example
119
+ const envExampleContent = `VITE_GOOGLE_MAPS_API_KEY=
120
+ VITE_GEMINI_API_KEY=`;
121
+ await fs.writeFile(path.join(targetDir, '.env.example'), envExampleContent);
122
+
99
123
  // Handle App.tsx specifically
100
124
  const pages = response.pages || [];
101
125
  const hasLogin = pages.includes('login');
@@ -138,11 +162,7 @@ export default function App() {
138
162
  if (basename === 'node_modules') return false;
139
163
 
140
164
  // Component filtering
141
- // src is absolute. We need relative validation.
142
- // Or just check if basename is in the list.
143
- // Warning: 'button.tsx' might be in other places? Unlikely in flat structure components/ui
144
- // Ideally we check if it is inside components/ui
145
- const isUI = src.includes(path.join('components', 'ui')); // Simple check
165
+ const isUI = src.includes(path.join('components', 'ui'));
146
166
 
147
167
  if (isUI && folder === 'components') {
148
168
  if (basename === 'index.ts') return true;
@@ -172,6 +192,22 @@ export default function App() {
172
192
  }
173
193
  }
174
194
 
195
+ // --- Apply Selected Theme ---
196
+ if (response.theme) {
197
+ const contextPath = path.join(targetDir, 'contexts', 'BrandColorsContext.tsx');
198
+ if (await fs.pathExists(contextPath)) {
199
+ let content = await fs.readFile(contextPath, 'utf8');
200
+ // Replace default theme
201
+ // Need to match the line: return saved || 'xertica-original';
202
+ // We can use regex to be safe
203
+ content = content.replace(
204
+ /return saved \|\| 'xertica-original';/,
205
+ `return saved || '${response.theme}';`
206
+ );
207
+ await fs.writeFile(contextPath, content);
208
+ }
209
+ }
210
+
175
211
  spinner.succeed('Project initialized successfully!');
176
212
 
177
213
  if (response.install) {
@@ -190,4 +226,80 @@ export default function App() {
190
226
  }
191
227
  });
192
228
 
229
+ program
230
+ .command('update')
231
+ .description('Update components in your project')
232
+ .argument('[components...]', 'Components to update')
233
+ .option('-a, --all', 'Update all installed components')
234
+ .action(async (components, options) => {
235
+ const targetDir = process.cwd();
236
+ const sourceRoot = path.resolve(__dirname, '../');
237
+ const uiComponentsDir = path.join(targetDir, 'components/ui');
238
+ const sourceUiDir = path.join(sourceRoot, 'components/ui');
239
+
240
+ if (!await fs.pathExists(uiComponentsDir)) {
241
+ console.error(chalk.red('Components directory not found. Is this a Xertica UI project?'));
242
+ return;
243
+ }
244
+
245
+ if (!await fs.pathExists(sourceUiDir)) {
246
+ console.error(chalk.red('Source components not found. Try reinstalling the CLI package.'));
247
+ return;
248
+ }
249
+
250
+ let componentsToUpdate: string[] = [];
251
+
252
+ if (options.all) {
253
+ const files = await fs.readdir(uiComponentsDir);
254
+ componentsToUpdate = files.filter(f => f.endsWith('.tsx'));
255
+ } else if (components && components.length > 0) {
256
+ componentsToUpdate = components.map((c: string) => c.endsWith('.tsx') ? c : `${c}.tsx`);
257
+ } else {
258
+ // Interactive mode
259
+ const files = await fs.readdir(uiComponentsDir);
260
+ const installedComponents = files.filter(f => f.endsWith('.tsx'));
261
+
262
+ if (installedComponents.length === 0) {
263
+ console.log(chalk.yellow('No components found to update.'));
264
+ return;
265
+ }
266
+
267
+ const response = await prompts({
268
+ type: 'multiselect',
269
+ name: 'selected',
270
+ message: 'Select components to update',
271
+ choices: installedComponents.map(f => ({ title: f, value: f })),
272
+ min: 1
273
+ });
274
+
275
+ if (!response.selected) return;
276
+ componentsToUpdate = response.selected;
277
+ }
278
+
279
+ const spinner = ora('Updating components...').start();
280
+
281
+ try {
282
+ let updatedCount = 0;
283
+ for (const component of componentsToUpdate) {
284
+ const srcPath = path.join(sourceUiDir, component);
285
+ const destPath = path.join(uiComponentsDir, component);
286
+
287
+ if (await fs.pathExists(srcPath)) {
288
+ await fs.copy(srcPath, destPath);
289
+ updatedCount++;
290
+ } else {
291
+ spinner.warn(`Component ${component} not found in source.`);
292
+ }
293
+ }
294
+ if (updatedCount > 0) {
295
+ spinner.succeed(`Successfully updated ${updatedCount} components!`);
296
+ } else {
297
+ spinner.info('No components updated.');
298
+ }
299
+ } catch (error) {
300
+ spinner.fail('Failed to update components');
301
+ console.error(error);
302
+ }
303
+ });
304
+
193
305
  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
 
@@ -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
- effectiveApiKey !== "YOUR_GOOGLE_MAPS_API_KEY_HERE" &&
374
- effectiveApiKey.startsWith("AIza");
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
- effectiveApiKey !== "YOUR_GOOGLE_MAPS_API_KEY_HERE" &&
208
- effectiveApiKey.startsWith("AIza");
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 (