@traky-app/ui 1.6.3 → 1.6.4

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 (60) hide show
  1. package/package.json +15 -1
  2. package/registry/traky/json/city-select.json +19 -0
  3. package/registry/traky/json/country-select.json +24 -0
  4. package/registry/traky/json/geo-search-select.json +24 -0
  5. package/registry/traky/json/geo-selectors.json +21 -0
  6. package/registry/traky/json/google-maps-address-picker.json +22 -0
  7. package/registry/traky/json/lib-geo-city-codes.json +14 -0
  8. package/registry/traky/json/lib-geo-countries.json +14 -0
  9. package/registry/traky/json/lib-geo-csc.json +17 -0
  10. package/registry/traky/json/lib-geo-locations.json +14 -0
  11. package/registry/traky/json/lib-geo-overrides.json +14 -0
  12. package/registry/traky/json/lib-geo-types.json +14 -0
  13. package/registry/traky/json/lib-google-maps-address.json +14 -0
  14. package/registry/traky/json/lib-phone-countries.json +14 -0
  15. package/registry/traky/json/lib-phone-e164.json +14 -0
  16. package/registry/traky/json/lib-utils.json +1 -1
  17. package/registry/traky/json/office-address-form-dialog.json +17 -0
  18. package/registry/traky/json/office-address-form.json +31 -0
  19. package/registry/traky/json/phone-number-input.json +25 -0
  20. package/registry/traky/json/state-select.json +19 -0
  21. package/registry/traky/lib/geo-city-codes.ts +234 -0
  22. package/registry/traky/lib/geo-countries.ts +75 -0
  23. package/registry/traky/lib/geo-csc.ts +82 -0
  24. package/registry/traky/lib/geo-locations.ts +171 -0
  25. package/registry/traky/lib/geo-overrides.ts +375 -0
  26. package/registry/traky/lib/geo-types.ts +34 -0
  27. package/registry/traky/lib/google-maps-address.ts +100 -0
  28. package/registry/traky/lib/phone-countries.ts +380 -0
  29. package/registry/traky/lib/phone-e164.ts +66 -0
  30. package/registry/traky/lib/utils.ts +24 -0
  31. package/registry/traky/registry.json +180 -0
  32. package/registry/traky/ui/city-select.tsx +113 -0
  33. package/registry/traky/ui/country-select.tsx +233 -0
  34. package/registry/traky/ui/geo-search-select.tsx +136 -0
  35. package/registry/traky/ui/geo-selectors.tsx +162 -0
  36. package/registry/traky/ui/google-maps-address-picker.tsx +422 -0
  37. package/registry/traky/ui/office-address-form-dialog.tsx +16 -0
  38. package/registry/traky/ui/office-address-form.tsx +868 -0
  39. package/registry/traky/ui/phone-number-input.tsx +253 -0
  40. package/registry/traky/ui/state-select.tsx +107 -0
  41. package/src/lib/geo-city-codes.ts +234 -0
  42. package/src/lib/geo-countries.ts +75 -0
  43. package/src/lib/geo-csc.ts +82 -0
  44. package/src/lib/geo-locations.ts +171 -0
  45. package/src/lib/geo-overrides.ts +375 -0
  46. package/src/lib/geo-types.ts +34 -0
  47. package/src/lib/google-maps-address.ts +100 -0
  48. package/src/lib/phone-countries.ts +380 -0
  49. package/src/lib/phone-e164.ts +66 -0
  50. package/src/test/setup.ts +31 -0
  51. package/src/ui/__tests__/geo-selects.test.tsx +504 -0
  52. package/src/ui/city-select.tsx +113 -0
  53. package/src/ui/country-select.tsx +233 -0
  54. package/src/ui/geo-search-select.tsx +136 -0
  55. package/src/ui/geo-selectors.tsx +162 -0
  56. package/src/ui/google-maps-address-picker.tsx +422 -0
  57. package/src/ui/office-address-form-dialog.tsx +16 -0
  58. package/src/ui/office-address-form.tsx +877 -0
  59. package/src/ui/phone-number-input.tsx +253 -0
  60. package/src/ui/state-select.tsx +107 -0
@@ -0,0 +1,162 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { CountrySelect, type Country } from "@/components/ui/country-select";
5
+ import { StateSelect } from "@/components/ui/state-select";
6
+ import { CitySelect } from "@/components/ui/city-select";
7
+ import type { GeoCityOption, GeoOption } from "@/lib/geo-types";
8
+ import { cn } from "@/lib/utils";
9
+ import { Label } from "@/components/ui/label";
10
+
11
+ export type GeoSelectorLevel = "country" | "state" | "city";
12
+
13
+ export type GeoSelectorsValue = {
14
+ country: Country | null;
15
+ state: GeoOption | null;
16
+ city: GeoCityOption | null;
17
+ };
18
+
19
+ const ALL_LEVELS: readonly GeoSelectorLevel[] = ["country", "state", "city"];
20
+
21
+ const EMPTY_VALUE: GeoSelectorsValue = {
22
+ country: null,
23
+ state: null,
24
+ city: null,
25
+ };
26
+
27
+ type GeoSelectorsProps = {
28
+ /**
29
+ * Niveles visibles. Omite los que no quieras.
30
+ * Si no está "country", pasa `countryCode`.
31
+ * Si no está "state" y sí "city", pasa `stateCode`.
32
+ */
33
+ levels?: readonly GeoSelectorLevel[];
34
+ /** País fijo cuando el select de país no se muestra. */
35
+ countryCode?: string | null;
36
+ /** Provincia fija cuando el select de provincia no se muestra. */
37
+ stateCode?: string | null;
38
+ onChange?: (value: GeoSelectorsValue) => void;
39
+ disabled?: boolean;
40
+ className?: string;
41
+ idPrefix?: string;
42
+ };
43
+
44
+ export function GeoSelectors({
45
+ levels = ALL_LEVELS,
46
+ countryCode,
47
+ stateCode,
48
+ onChange,
49
+ disabled = false,
50
+ className,
51
+ idPrefix = "geo",
52
+ }: GeoSelectorsProps) {
53
+ const showCountry = levels.includes("country");
54
+ const showState = levels.includes("state");
55
+ const showCity = levels.includes("city");
56
+ const [selection, setSelection] = useState<GeoSelectorsValue>(EMPTY_VALUE);
57
+
58
+ const resolvedCountry = showCountry
59
+ ? (selection.country?.code ?? "")
60
+ : (countryCode?.trim().toUpperCase() ?? "");
61
+ const resolvedState = showState
62
+ ? (selection.state?.code ?? "")
63
+ : (stateCode?.trim() ?? "");
64
+
65
+ const externalCountry = countryCode?.trim().toUpperCase() ?? "";
66
+ const externalState = stateCode?.trim() ?? "";
67
+
68
+ useEffect(() => {
69
+ if (showCountry) return;
70
+ setSelection((prev) => {
71
+ if (!prev.state && !prev.city) return prev;
72
+ return { ...prev, state: null, city: null };
73
+ });
74
+ }, [externalCountry, showCountry]);
75
+
76
+ useEffect(() => {
77
+ if (showState) return;
78
+ setSelection((prev) => {
79
+ if (!prev.city) return prev;
80
+ return { ...prev, city: null };
81
+ });
82
+ }, [externalState, showState]);
83
+
84
+ const publish = (next: GeoSelectorsValue) => {
85
+ setSelection(next);
86
+ onChange?.(next);
87
+ };
88
+
89
+ const visible = [showCountry, showState, showCity].filter(Boolean).length;
90
+ if (visible === 0) return null;
91
+
92
+ return (
93
+ <div
94
+ className={cn(
95
+ "grid gap-4",
96
+ visible >= 3
97
+ ? "md:grid-cols-3"
98
+ : visible === 2
99
+ ? "md:grid-cols-2"
100
+ : "max-w-sm",
101
+ className,
102
+ )}
103
+ >
104
+ {showCountry ? (
105
+ <div className="space-y-1">
106
+ <Label htmlFor={`${idPrefix}-country`}>País</Label>
107
+ <CountrySelect
108
+ id={`${idPrefix}-country`}
109
+ className="w-full"
110
+ size="lg"
111
+ value={selection.country?.code}
112
+ disabled={disabled}
113
+ onChange={(country) =>
114
+ publish({ country, state: null, city: null })
115
+ }
116
+ allowClear
117
+ onClear={() => publish(EMPTY_VALUE)}
118
+ />
119
+ </div>
120
+ ) : null}
121
+
122
+ {showState ? (
123
+ <div className="space-y-1">
124
+ <Label htmlFor={`${idPrefix}-state`}>Estado / Región / Municipio</Label>
125
+ <StateSelect
126
+ id={`${idPrefix}-state`}
127
+ countryCode={resolvedCountry}
128
+ value={selection.state?.code ?? ""}
129
+ disabled={disabled}
130
+ onChange={(state) =>
131
+ publish({
132
+ country: selection.country,
133
+ state,
134
+ city: null,
135
+ })
136
+ }
137
+ />
138
+ </div>
139
+ ) : null}
140
+
141
+ {showCity ? (
142
+ <div className="space-y-1">
143
+ <Label htmlFor={`${idPrefix}-city`}>Ciudad / Pueblo / Distrito</Label>
144
+ <CitySelect
145
+ id={`${idPrefix}-city`}
146
+ countryCode={resolvedCountry}
147
+ stateCode={resolvedState}
148
+ value={selection.city?.code ?? selection.city?.name ?? ""}
149
+ disabled={disabled}
150
+ onChange={(city) =>
151
+ publish({
152
+ country: selection.country,
153
+ state: selection.state,
154
+ city,
155
+ })
156
+ }
157
+ />
158
+ </div>
159
+ ) : null}
160
+ </div>
161
+ );
162
+ }
@@ -0,0 +1,422 @@
1
+ "use client"
2
+
3
+ import { useCallback, useEffect, useMemo, useRef, useState } from "react"
4
+ import { useJsApiLoader, GoogleMap, Marker } from "@react-google-maps/api"
5
+ import { IconLoader } from "@tabler/icons-react"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import {
9
+ parseGoogleAddressComponents,
10
+ type GoogleMapsAddressSelection,
11
+ } from "@/lib/google-maps-address"
12
+ import { Label } from "@/components/ui/label"
13
+
14
+ const MAP_LIBRARIES: ("places")[] = ["places"]
15
+ const DEFAULT_CENTER = { lat: -1.8312, lng: -78.1834 }
16
+ const DEFAULT_ZOOM = 6
17
+ const SELECTED_ZOOM = 16
18
+ const MAP_CONTAINER_STYLE = { width: "100%", height: "min(52dvh, 360px)" }
19
+
20
+ function applyGooglePlacesLightTheme(
21
+ element: google.maps.places.PlaceAutocompleteElement,
22
+ ) {
23
+ element.style.setProperty("color-scheme", "light")
24
+ element.style.setProperty("background-color", "#ffffff")
25
+ element.style.setProperty("color", "#141414")
26
+ element.style.setProperty("border", "1px solid #e2e8f0")
27
+ element.style.setProperty("border-radius", "0.375rem")
28
+ element.style.setProperty("width", "100%")
29
+ element.style.setProperty("min-height", "2.75rem")
30
+ element.style.setProperty("font-size", "0.875rem")
31
+ }
32
+
33
+ function parsePlaceInstance(
34
+ place: google.maps.places.Place,
35
+ ): GoogleMapsAddressSelection | null {
36
+ const lat = place.location?.lat()
37
+ const lng = place.location?.lng()
38
+ if (lat == null || lng == null) return null
39
+
40
+ const parsed = parseGoogleAddressComponents(
41
+ place.addressComponents as Parameters<
42
+ typeof parseGoogleAddressComponents
43
+ >[0],
44
+ place.displayName || place.formattedAddress || "",
45
+ lat,
46
+ lng,
47
+ )
48
+ if (!parsed) return null
49
+ return {
50
+ ...parsed,
51
+ formattedAddress: place.formattedAddress?.trim() || undefined,
52
+ }
53
+ }
54
+
55
+ export type GoogleMapsAddressPickerProps = {
56
+ onLocationSelect: (address: GoogleMapsAddressSelection) => void
57
+ onLocationClear?: () => void
58
+ regionCode?: string
59
+ className?: string
60
+ initialLatitude?: number
61
+ initialLongitude?: number
62
+ initialFormattedAddress?: string
63
+ /** Prioridad sobre `NEXT_PUBLIC_GOOGLE_MAPS_API_KEY`. */
64
+ apiKey?: string
65
+ }
66
+
67
+ function GoogleMapsAddressPickerInner({
68
+ onLocationSelect,
69
+ onLocationClear,
70
+ regionCode,
71
+ className,
72
+ initialLatitude,
73
+ initialLongitude,
74
+ initialFormattedAddress,
75
+ }: Omit<GoogleMapsAddressPickerProps, "apiKey">) {
76
+ const autocompleteHostRef = useRef<HTMLDivElement>(null)
77
+ const autocompleteElementRef =
78
+ useRef<google.maps.places.PlaceAutocompleteElement | null>(null)
79
+ const geocoderRef = useRef<google.maps.Geocoder | null>(null)
80
+
81
+ const [searchLabel, setSearchLabel] = useState("")
82
+ const [isGeocoding, setIsGeocoding] = useState(false)
83
+ const [geocodeError, setGeocodeError] = useState<string | null>(null)
84
+ const [marker, setMarker] = useState<google.maps.LatLngLiteral | null>(() => {
85
+ if (
86
+ typeof initialLatitude === "number" &&
87
+ typeof initialLongitude === "number"
88
+ ) {
89
+ return { lat: initialLatitude, lng: initialLongitude }
90
+ }
91
+ return null
92
+ })
93
+ const [mapCenter, setMapCenter] = useState<google.maps.LatLngLiteral>(
94
+ marker ?? DEFAULT_CENTER,
95
+ )
96
+ const [mapZoom, setMapZoom] = useState(marker ? SELECTED_ZOOM : DEFAULT_ZOOM)
97
+
98
+ useEffect(() => {
99
+ const lat =
100
+ typeof initialLatitude === "number" && Number.isFinite(initialLatitude)
101
+ ? initialLatitude
102
+ : null
103
+ const lng =
104
+ typeof initialLongitude === "number" && Number.isFinite(initialLongitude)
105
+ ? initialLongitude
106
+ : null
107
+ if (lat == null || lng == null) return
108
+ const position = { lat, lng }
109
+ setMarker(position)
110
+ setMapCenter(position)
111
+ setMapZoom(SELECTED_ZOOM)
112
+ }, [initialLatitude, initialLongitude])
113
+
114
+ useEffect(() => {
115
+ const label = initialFormattedAddress?.trim()
116
+ if (!label) return
117
+ setSearchLabel(label)
118
+ if (autocompleteElementRef.current) {
119
+ autocompleteElementRef.current.value = label
120
+ }
121
+ }, [initialFormattedAddress])
122
+
123
+ const mapOptions = useMemo<google.maps.MapOptions>(
124
+ () => ({
125
+ streetViewControl: false,
126
+ mapTypeControl: false,
127
+ fullscreenControl: false,
128
+ zoomControl: true,
129
+ }),
130
+ [],
131
+ )
132
+
133
+ const applySelection = useCallback(
134
+ (address: GoogleMapsAddressSelection, label: string) => {
135
+ const position = { lat: address.latitude, lng: address.longitude }
136
+ setMarker(position)
137
+ setMapCenter(position)
138
+ setMapZoom(SELECTED_ZOOM)
139
+ setSearchLabel(label)
140
+ setGeocodeError(null)
141
+ if (autocompleteElementRef.current) {
142
+ autocompleteElementRef.current.value = label
143
+ }
144
+ onLocationSelect({
145
+ ...address,
146
+ formattedAddress: label.trim() || address.formattedAddress,
147
+ })
148
+ },
149
+ [onLocationSelect],
150
+ )
151
+
152
+ const reverseGeocode = useCallback(
153
+ (position: google.maps.LatLngLiteral) =>
154
+ new Promise<{ address: GoogleMapsAddressSelection; label: string } | null>(
155
+ (resolve) => {
156
+ if (!geocoderRef.current) {
157
+ geocoderRef.current = new google.maps.Geocoder()
158
+ }
159
+ geocoderRef.current.geocode({ location: position }, (results, status) => {
160
+ if (status !== "OK" || !results?.[0]) {
161
+ resolve(null)
162
+ return
163
+ }
164
+ const result = results[0]
165
+ const parsed = parseGoogleAddressComponents(
166
+ result.address_components,
167
+ result.formatted_address || "",
168
+ position.lat,
169
+ position.lng,
170
+ )
171
+ if (!parsed) {
172
+ resolve(null)
173
+ return
174
+ }
175
+ const label = result.formatted_address?.trim() || ""
176
+ resolve({
177
+ address: {
178
+ ...parsed,
179
+ formattedAddress: label || undefined,
180
+ },
181
+ label,
182
+ })
183
+ })
184
+ },
185
+ ),
186
+ [],
187
+ )
188
+
189
+ const handlePositionChange = useCallback(
190
+ async (position: google.maps.LatLngLiteral) => {
191
+ setMarker(position)
192
+ setMapCenter(position)
193
+ setMapZoom(SELECTED_ZOOM)
194
+ setIsGeocoding(true)
195
+ setGeocodeError(null)
196
+
197
+ const resolved = await reverseGeocode(position)
198
+ setIsGeocoding(false)
199
+
200
+ if (resolved) {
201
+ applySelection(resolved.address, resolved.label)
202
+ return
203
+ }
204
+
205
+ setGeocodeError(
206
+ "No pudimos resolver esa ubicación. Elige una dirección en el buscador o activa Geocoding API en Google Cloud.",
207
+ )
208
+ },
209
+ [applySelection, reverseGeocode],
210
+ )
211
+
212
+ useEffect(() => {
213
+ const host = autocompleteHostRef.current
214
+ if (!host) return
215
+
216
+ let cancelled = false
217
+ let placeAutocomplete: google.maps.places.PlaceAutocompleteElement | null =
218
+ null
219
+
220
+ const onSelect: EventListener = (event) => {
221
+ void (async () => {
222
+ const selectEvent =
223
+ event as google.maps.places.PlacePredictionSelectEvent
224
+ const place = selectEvent.placePrediction.toPlace()
225
+ await place.fetchFields({
226
+ fields: [
227
+ "displayName",
228
+ "formattedAddress",
229
+ "location",
230
+ "addressComponents",
231
+ ],
232
+ })
233
+
234
+ const parsed = parsePlaceInstance(place)
235
+ if (!parsed) return
236
+
237
+ const label =
238
+ place.formattedAddress?.trim() ||
239
+ [
240
+ parsed.streetNumber,
241
+ parsed.street,
242
+ parsed.city,
243
+ parsed.state,
244
+ parsed.postalCode,
245
+ ]
246
+ .filter(Boolean)
247
+ .join(", ")
248
+
249
+ applySelection(parsed, label)
250
+ })()
251
+ }
252
+
253
+ void (async () => {
254
+ await google.maps.importLibrary("places")
255
+
256
+ if (cancelled || !autocompleteHostRef.current) return
257
+
258
+ const region = regionCode?.trim().toLowerCase()
259
+ placeAutocomplete = new google.maps.places.PlaceAutocompleteElement({
260
+ ...(region ? { includedRegionCodes: [region] } : {}),
261
+ placeholder: "Busca tu dirección en Google Maps…",
262
+ })
263
+
264
+ placeAutocomplete.className = "gmp-place-autocomplete-host block w-full"
265
+ applyGooglePlacesLightTheme(placeAutocomplete)
266
+
267
+ const savedLabel = initialFormattedAddress?.trim()
268
+ if (savedLabel) {
269
+ placeAutocomplete.value = savedLabel
270
+ setSearchLabel(savedLabel)
271
+ }
272
+
273
+ host.replaceChildren(placeAutocomplete)
274
+ autocompleteElementRef.current = placeAutocomplete
275
+ placeAutocomplete.addEventListener("gmp-select", onSelect)
276
+ })()
277
+
278
+ return () => {
279
+ cancelled = true
280
+ if (placeAutocomplete) {
281
+ placeAutocomplete.removeEventListener("gmp-select", onSelect)
282
+ }
283
+ host.replaceChildren()
284
+ autocompleteElementRef.current = null
285
+ }
286
+ }, [applySelection, initialFormattedAddress, regionCode])
287
+
288
+ const handleClear = () => {
289
+ setMarker(null)
290
+ setSearchLabel("")
291
+ setGeocodeError(null)
292
+ setIsGeocoding(false)
293
+ setMapCenter(DEFAULT_CENTER)
294
+ setMapZoom(DEFAULT_ZOOM)
295
+ if (autocompleteElementRef.current) {
296
+ autocompleteElementRef.current.value = ""
297
+ }
298
+ onLocationClear?.()
299
+ }
300
+
301
+ return (
302
+ <div className={cn("space-y-3", className)}>
303
+ <div className="space-y-1.5">
304
+ <Label>Buscar dirección</Label>
305
+ <div
306
+ ref={autocompleteHostRef}
307
+ className="gmp-autocomplete-wrapper w-full rounded-md bg-background"
308
+ />
309
+ </div>
310
+
311
+ <div className="overflow-hidden rounded-lg border border-border">
312
+ <GoogleMap
313
+ mapContainerStyle={MAP_CONTAINER_STYLE}
314
+ center={mapCenter}
315
+ zoom={mapZoom}
316
+ onClick={(event) => {
317
+ const latLng = event.latLng
318
+ if (!latLng) return
319
+ void handlePositionChange({ lat: latLng.lat(), lng: latLng.lng() })
320
+ }}
321
+ options={mapOptions}
322
+ >
323
+ {marker ? (
324
+ <Marker
325
+ position={marker}
326
+ draggable
327
+ onDragEnd={(event) => {
328
+ const latLng = event.latLng
329
+ if (!latLng) return
330
+ void handlePositionChange({
331
+ lat: latLng.lat(),
332
+ lng: latLng.lng(),
333
+ })
334
+ }}
335
+ />
336
+ ) : null}
337
+ </GoogleMap>
338
+ </div>
339
+
340
+ {geocodeError ? (
341
+ <p className="text-xs text-destructive">{geocodeError}</p>
342
+ ) : null}
343
+
344
+ {marker ? (
345
+ <div className="flex flex-wrap items-center justify-between gap-2 text-xs text-muted-foreground">
346
+ <span className="min-w-0 break-words">
347
+ {isGeocoding ? (
348
+ <>
349
+ <IconLoader className="mr-1 inline size-3 animate-spin" />
350
+ Actualizando dirección…
351
+ </>
352
+ ) : (
353
+ searchLabel ||
354
+ `${marker.lat.toFixed(5)}, ${marker.lng.toFixed(5)}`
355
+ )}
356
+ </span>
357
+ <button
358
+ type="button"
359
+ className="shrink-0 text-primary underline underline-offset-2"
360
+ onClick={handleClear}
361
+ >
362
+ Limpiar
363
+ </button>
364
+ </div>
365
+ ) : (
366
+ <p className="text-xs text-muted-foreground">
367
+ Busca una dirección o haz clic en el mapa para fijar el punto.
368
+ </p>
369
+ )}
370
+ </div>
371
+ )
372
+ }
373
+
374
+ function resolveGoogleMapsApiKey(apiKey?: string): string {
375
+ if (apiKey?.trim()) return apiKey.trim()
376
+ if (typeof process !== "undefined" && process.env) {
377
+ return process.env.NEXT_PUBLIC_GOOGLE_MAPS_API_KEY?.trim() ?? ""
378
+ }
379
+ return ""
380
+ }
381
+
382
+ export function GoogleMapsAddressPicker({
383
+ apiKey,
384
+ ...props
385
+ }: GoogleMapsAddressPickerProps) {
386
+ const resolvedKey = resolveGoogleMapsApiKey(apiKey)
387
+
388
+ const { isLoaded, loadError } = useJsApiLoader({
389
+ googleMapsApiKey: resolvedKey,
390
+ libraries: MAP_LIBRARIES,
391
+ })
392
+
393
+ if (!resolvedKey) {
394
+ return (
395
+ <div className="rounded-lg border border-dashed border-border bg-muted/30 p-4 text-sm text-muted-foreground">
396
+ Configura{" "}
397
+ <code className="text-xs">NEXT_PUBLIC_GOOGLE_MAPS_API_KEY</code> (solo
398
+ desarrollo) para usar búsqueda y mapa de Google.
399
+ </div>
400
+ )
401
+ }
402
+
403
+ if (loadError) {
404
+ return (
405
+ <div className="rounded-lg border border-destructive/40 bg-destructive/10 p-4 text-sm text-destructive">
406
+ No se pudo cargar Google Maps. Habilita Maps JavaScript API, Places API
407
+ (New) y Geocoding API para esta clave.
408
+ </div>
409
+ )
410
+ }
411
+
412
+ if (!isLoaded) {
413
+ return (
414
+ <div className="flex h-[min(52dvh,360px)] items-center justify-center rounded-lg border bg-muted/30 text-sm text-muted-foreground">
415
+ <IconLoader className="mr-2 size-4 animate-spin" />
416
+ Cargando mapa…
417
+ </div>
418
+ )
419
+ }
420
+
421
+ return <GoogleMapsAddressPickerInner {...props} />
422
+ }
@@ -0,0 +1,16 @@
1
+ "use client"
2
+
3
+ /** Compat: importa desde `@traky-app/ui/office-address-form` (`surface="modal"`). */
4
+ export {
5
+ OfficeAddressForm,
6
+ OfficeAddressForm as OfficeAddressFormDialog,
7
+ OfficeAddressFormCard,
8
+ useOfficeAddressForm,
9
+ EMPTY_OFFICE_ADDRESS_FORM_VALUES,
10
+ mergeOfficeAddressFormValues,
11
+ type OfficeAddressFormProps,
12
+ type OfficeAddressFormDialogProps,
13
+ type OfficeAddressFormValues,
14
+ type OfficeAddressEntryMode,
15
+ type OfficeAddressFormCardProps,
16
+ } from "@/components/ui/office-address-form"