pixelon 0.5.0 → 0.5.1
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/dist/components/molecules/chat/StructuredContent/components/LocationField/usePlaces.d.ts +11 -0
- package/dist/components/molecules/chat/StructuredContent/components/ServicesMap/types.d.ts +6 -0
- package/dist/components/organisms/chat/Chat/hooks/useAutoScroll.d.ts +8 -1
- package/dist/components/templates/lumi/Crud/helpers/confirmSummary.d.ts +13 -0
- package/dist/components/templates/lumi/Crud/types.d.ts +6 -0
- package/dist/index.esm.js +122 -12
- package/dist/index.js +122 -12
- package/dist/styles.css +1 -1
- package/dist/styles.css.map +1 -1
- package/package.json +1 -1
package/dist/components/molecules/chat/StructuredContent/components/LocationField/usePlaces.d.ts
CHANGED
|
@@ -11,6 +11,17 @@ export declare function usePlaceSuggestions(query: string, enabled: boolean): {
|
|
|
11
11
|
} | null>;
|
|
12
12
|
clear: () => void;
|
|
13
13
|
};
|
|
14
|
+
/** SPEC-131 T6 — la dirección formateada de Google repite tramos.
|
|
15
|
+
*
|
|
16
|
+
* Medido: el `formattedAddress` de una calle de El Poblado vuelve como
|
|
17
|
+
* «Cl 10 #43-20, El Poblado, Medellín, El Poblado, Medellín, Antioquia», porque el nombre del
|
|
18
|
+
* lugar ya trae el barrio y la ciudad que luego añade la parte administrativa. Esa cadena se
|
|
19
|
+
* guarda tal cual y aparece en el chat, en la ficha, en la tabla y en Conversaciones.
|
|
20
|
+
*
|
|
21
|
+
* Se comparan los tramos sin tildes ni mayúsculas, que es donde estaba el otro caso visto
|
|
22
|
+
* («Itagüí, Itagüi»), y se conserva el primero de cada uno: el orden de una dirección va de lo
|
|
23
|
+
* particular a lo general, así que el que sobra siempre es el segundo. */
|
|
24
|
+
export declare function limpiarDireccion(address: string): string;
|
|
14
25
|
/**
|
|
15
26
|
* Traduce una coordenada a una dirección legible.
|
|
16
27
|
* Requiere Geocoding habilitado en la clave del navegador; si no lo está, devuelve null y el
|
|
@@ -15,6 +15,12 @@ export interface ServicesMapConfig {
|
|
|
15
15
|
data: {
|
|
16
16
|
list: EndpointRef;
|
|
17
17
|
};
|
|
18
|
+
/** SPEC-131 T6 — qué pide el mapa cuando nadie acota nada.
|
|
19
|
+
*
|
|
20
|
+
* Un mapa de seguimiento sin filtro devuelve el historial entero, y eso es lo que recibía quien
|
|
21
|
+
* preguntaba «¿ya viene mi grúa?». El `filter` de la instancia PISA esto: cuando quien emite el
|
|
22
|
+
* componente sí sabe qué acotar, manda él. */
|
|
23
|
+
defaultFilter?: Record<string, unknown>;
|
|
18
24
|
mapping: ServicesMapMapping;
|
|
19
25
|
/** Icono para todos los puntos cuando la fila no trae uno. */
|
|
20
26
|
defaultIcon?: string;
|
|
@@ -14,9 +14,16 @@ export interface UseAutoScrollOptions {
|
|
|
14
14
|
* desconectarlo), dos envíos seguidos dejaban dos observadores compitiendo por mover el scroll,
|
|
15
15
|
* y ninguno se limpiaba al desmontar.
|
|
16
16
|
*/
|
|
17
|
+
/** SPEC-131 T3 — cuánto espacio hay que reservar al final para que el objetivo pueda subir arriba.
|
|
18
|
+
*
|
|
19
|
+
* Lo que falte y nada más: si desde el objetivo hasta el final ya hay una pantalla de contenido,
|
|
20
|
+
* no hace falta reservar nada. Reservar `clientHeight` a ciegas mete una pantalla de blanco al
|
|
21
|
+
* final, y con el scroll topado eso deja la conversación entera por encima del borde superior. */
|
|
22
|
+
export declare function reservaNecesaria(clientHeight: number, scrollHeight: number, targetTop: number): number;
|
|
17
23
|
export declare const useAutoScroll: ({ messagesEndRef, messagesContainerRef, }: UseAutoScrollOptions) => {
|
|
18
24
|
scrollToLastUserMessage: () => void;
|
|
19
25
|
pinLastUserMessage: () => void;
|
|
20
26
|
scrollToProactive: () => void;
|
|
21
|
-
scrollToBottom: () => void;
|
|
27
|
+
scrollToBottom: (suave?: boolean) => void;
|
|
28
|
+
aterrizarAlFinal: () => void;
|
|
22
29
|
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export interface LineaResumen {
|
|
2
|
+
label: string;
|
|
3
|
+
valor: string;
|
|
4
|
+
}
|
|
5
|
+
interface CampoResumible {
|
|
6
|
+
name: string;
|
|
7
|
+
label?: string;
|
|
8
|
+
moleculeType?: string;
|
|
9
|
+
uiProps?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
/** Lo que se va a enviar, en el orden del formulario y sin los campos vacíos. */
|
|
12
|
+
export declare function resumenParaConfirmar(campos: ReadonlyArray<CampoResumible>, datos: Record<string, unknown> | null | undefined): LineaResumen[];
|
|
13
|
+
export {};
|
|
@@ -51,6 +51,12 @@ export interface RenderFieldConfig {
|
|
|
51
51
|
formatValue?: (value: unknown, row: unknown) => React.ReactNode;
|
|
52
52
|
/** Función para parsear el valor antes de submit */
|
|
53
53
|
parseValue?: (value: unknown) => unknown;
|
|
54
|
+
/** SPEC-131 T7 — cuántas columnas ocupa el campo en el formulario.
|
|
55
|
+
*
|
|
56
|
+
* En un grid las celdas de una fila comparten altura, así que un campo que se despliega —el de
|
|
57
|
+
* ubicación, que abre un mapa de verificación al elegir la dirección— deja a su hermana con
|
|
58
|
+
* cientos de píxeles de blanco al lado. Los campos que crecen ocupan la fila entera. */
|
|
59
|
+
gridSpan?: number;
|
|
54
60
|
}
|
|
55
61
|
/**
|
|
56
62
|
* Estado de paginación
|
package/dist/index.esm.js
CHANGED
|
@@ -10924,6 +10924,7 @@ const Drawer = ({
|
|
|
10924
10924
|
"data-testid": dataTestId,
|
|
10925
10925
|
scrollIndicatorOffset = 1
|
|
10926
10926
|
}) => {
|
|
10927
|
+
const idTitulo = useId();
|
|
10927
10928
|
const [isMobile, setIsMobile] = useState(false);
|
|
10928
10929
|
const contentRef = useRef(null);
|
|
10929
10930
|
const [showScrollIndicator, setShowScrollIndicator] = useState(false);
|
|
@@ -11047,14 +11048,14 @@ const Drawer = ({
|
|
|
11047
11048
|
style: getDrawerStyles(),
|
|
11048
11049
|
role: "dialog",
|
|
11049
11050
|
"aria-modal": "true",
|
|
11050
|
-
"aria-labelledby": title ?
|
|
11051
|
+
"aria-labelledby": title ? idTitulo : void 0,
|
|
11051
11052
|
"data-testid": dataTestId,
|
|
11052
11053
|
children: /* @__PURE__ */ jsxs("div", { className: `${COMPONENT_PREFIX}drawer__viewport`, children: [
|
|
11053
11054
|
(title || showCloseButton || headerExtra) && /* @__PURE__ */ jsxs("div", { className: `${COMPONENT_PREFIX}drawer__header`, children: [
|
|
11054
11055
|
title && /* @__PURE__ */ jsx(
|
|
11055
11056
|
"h2",
|
|
11056
11057
|
{
|
|
11057
|
-
id:
|
|
11058
|
+
id: idTitulo,
|
|
11058
11059
|
className: `${COMPONENT_PREFIX}drawer__title`,
|
|
11059
11060
|
children: title
|
|
11060
11061
|
}
|
|
@@ -13167,7 +13168,9 @@ function mapFieldsToFormFields(fields, mode) {
|
|
|
13167
13168
|
readOnly: field.readOnly,
|
|
13168
13169
|
placeholder: field.placeholder,
|
|
13169
13170
|
uiProps: field.uiProps,
|
|
13170
|
-
transformOnSubmit: field.parseValue
|
|
13171
|
+
transformOnSubmit: field.parseValue,
|
|
13172
|
+
gridSpan: field.gridSpan
|
|
13173
|
+
// SPEC-131 T7
|
|
13171
13174
|
}));
|
|
13172
13175
|
}
|
|
13173
13176
|
|
|
@@ -13347,6 +13350,47 @@ function calculateTotalPages(totalRecords, limit) {
|
|
|
13347
13350
|
return Math.ceil(totalRecords / limit);
|
|
13348
13351
|
}
|
|
13349
13352
|
|
|
13353
|
+
function etiquetaDeOpcion(valor, uiProps) {
|
|
13354
|
+
const opciones = uiProps?.options;
|
|
13355
|
+
if (!Array.isArray(opciones)) return null;
|
|
13356
|
+
const encontrada = opciones.find((o) => o && String(o.value) === String(valor));
|
|
13357
|
+
return encontrada?.label != null ? String(encontrada.label) : null;
|
|
13358
|
+
}
|
|
13359
|
+
function esNumerico(moleculeType) {
|
|
13360
|
+
return moleculeType === "INPUT_CURRENCY" || moleculeType === "INPUT_NUMBER";
|
|
13361
|
+
}
|
|
13362
|
+
function aTexto(valor, uiProps, moleculeType) {
|
|
13363
|
+
if (valor == null || valor === "") return "";
|
|
13364
|
+
if (typeof valor === "boolean") return valor ? "S\xED" : "No";
|
|
13365
|
+
if (esNumerico(moleculeType)) {
|
|
13366
|
+
const n = Number(String(valor).replace(/[^\d.-]/g, ""));
|
|
13367
|
+
if (Number.isFinite(n)) return n.toLocaleString();
|
|
13368
|
+
}
|
|
13369
|
+
if (typeof File !== "undefined" && valor instanceof File) return valor.name;
|
|
13370
|
+
if (Array.isArray(valor)) {
|
|
13371
|
+
const partes = valor.map((v) => aTexto(v, uiProps, moleculeType)).filter(Boolean);
|
|
13372
|
+
return partes.join(", ");
|
|
13373
|
+
}
|
|
13374
|
+
if (typeof valor === "object") {
|
|
13375
|
+
const o = valor;
|
|
13376
|
+
for (const clave of ["label", "address", "name", "text", "value"]) {
|
|
13377
|
+
if (typeof o[clave] === "string" && o[clave]) return o[clave];
|
|
13378
|
+
}
|
|
13379
|
+
return "";
|
|
13380
|
+
}
|
|
13381
|
+
return etiquetaDeOpcion(valor, uiProps) ?? String(valor);
|
|
13382
|
+
}
|
|
13383
|
+
function resumenParaConfirmar(campos, datos) {
|
|
13384
|
+
if (!datos) return [];
|
|
13385
|
+
const lineas = [];
|
|
13386
|
+
for (const campo of campos) {
|
|
13387
|
+
const valor = aTexto(datos[campo.name], campo.uiProps, campo.moleculeType);
|
|
13388
|
+
if (!valor) continue;
|
|
13389
|
+
lineas.push({ label: campo.label ?? campo.name, valor });
|
|
13390
|
+
}
|
|
13391
|
+
return lineas;
|
|
13392
|
+
}
|
|
13393
|
+
|
|
13350
13394
|
const useAnchoPropio = () => {
|
|
13351
13395
|
const ref = useRef(null);
|
|
13352
13396
|
const [ancho, setAncho] = useState(null);
|
|
@@ -13791,6 +13835,10 @@ function Crud({
|
|
|
13791
13835
|
return "Formulario";
|
|
13792
13836
|
}
|
|
13793
13837
|
};
|
|
13838
|
+
const resumenConfirmacion = useMemo(
|
|
13839
|
+
() => confirmAction === "create" || confirmAction === "edit" ? resumenParaConfirmar(formFields, pendingFormData) : [],
|
|
13840
|
+
[confirmAction, formFields, pendingFormData]
|
|
13841
|
+
);
|
|
13794
13842
|
const getConfirmMessage = () => {
|
|
13795
13843
|
switch (confirmAction) {
|
|
13796
13844
|
case "create":
|
|
@@ -13930,6 +13978,10 @@ function Crud({
|
|
|
13930
13978
|
className: `${COMPONENT_PREFIX}crud__confirm-drawer`,
|
|
13931
13979
|
children: /* @__PURE__ */ jsxs("div", { className: `${COMPONENT_PREFIX}crud__confirm-content`, children: [
|
|
13932
13980
|
/* @__PURE__ */ jsx("p", { className: `${COMPONENT_PREFIX}crud__confirm-message`, children: getConfirmMessage() }),
|
|
13981
|
+
resumenConfirmacion.length > 0 && /* @__PURE__ */ jsx("dl", { className: `${COMPONENT_PREFIX}crud__confirm-summary`, children: resumenConfirmacion.map((linea) => /* @__PURE__ */ jsxs("div", { className: `${COMPONENT_PREFIX}crud__confirm-summary-row`, children: [
|
|
13982
|
+
/* @__PURE__ */ jsx("dt", { children: linea.label }),
|
|
13983
|
+
/* @__PURE__ */ jsx("dd", { children: linea.valor })
|
|
13984
|
+
] }, linea.label)) }),
|
|
13933
13985
|
/* @__PURE__ */ jsxs("div", { className: `${COMPONENT_PREFIX}crud__confirm-actions`, children: [
|
|
13934
13986
|
/* @__PURE__ */ jsx(
|
|
13935
13987
|
Button,
|
|
@@ -15875,7 +15927,8 @@ function usePlaceSuggestions(query, enabled) {
|
|
|
15875
15927
|
return {
|
|
15876
15928
|
lat: typeof loc.lat === "function" ? loc.lat() : loc.lat,
|
|
15877
15929
|
lng: typeof loc.lng === "function" ? loc.lng() : loc.lng,
|
|
15878
|
-
address: String(place.formattedAddress ?? "")
|
|
15930
|
+
address: limpiarDireccion(String(place.formattedAddress ?? ""))
|
|
15931
|
+
// SPEC-131 T6
|
|
15879
15932
|
};
|
|
15880
15933
|
} catch {
|
|
15881
15934
|
return null;
|
|
@@ -15885,13 +15938,27 @@ function usePlaceSuggestions(query, enabled) {
|
|
|
15885
15938
|
);
|
|
15886
15939
|
return { suggestions, resolve, clear: () => setSuggestions([]) };
|
|
15887
15940
|
}
|
|
15941
|
+
function limpiarDireccion(address) {
|
|
15942
|
+
const normal = (s) => s.trim().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
15943
|
+
const vistos = /* @__PURE__ */ new Set();
|
|
15944
|
+
const tramos = [];
|
|
15945
|
+
for (const tramo of address.split(",")) {
|
|
15946
|
+
const limpio = tramo.trim();
|
|
15947
|
+
if (!limpio) continue;
|
|
15948
|
+
const clave = normal(limpio);
|
|
15949
|
+
if (vistos.has(clave)) continue;
|
|
15950
|
+
vistos.add(clave);
|
|
15951
|
+
tramos.push(limpio);
|
|
15952
|
+
}
|
|
15953
|
+
return tramos.join(", ");
|
|
15954
|
+
}
|
|
15888
15955
|
function reverseGeocode(lat, lng) {
|
|
15889
15956
|
return new Promise((resolve) => {
|
|
15890
15957
|
const maps = window.google?.maps;
|
|
15891
15958
|
if (!maps?.Geocoder) return resolve(null);
|
|
15892
15959
|
new maps.Geocoder().geocode({ location: { lat, lng } }, (results, status) => {
|
|
15893
15960
|
if (status !== "OK" || !results?.length) return resolve(null);
|
|
15894
|
-
resolve(String(results[0].formatted_address ?? "") || null);
|
|
15961
|
+
resolve(limpiarDireccion(String(results[0].formatted_address ?? "")) || null);
|
|
15895
15962
|
});
|
|
15896
15963
|
});
|
|
15897
15964
|
}
|
|
@@ -16048,6 +16115,10 @@ function toRenderField(f, loaded, eventBus, mapsApiKey, loadError) {
|
|
|
16048
16115
|
showInModes: f.showInModes,
|
|
16049
16116
|
required: f.required,
|
|
16050
16117
|
placeholder: f.placeholder,
|
|
16118
|
+
// SPEC-131 T7 — la fila entera: al elegir la dirección se despliega el mapa de verificación
|
|
16119
|
+
// y, a media fila, su campo hermano se quedaba con ~400px de blanco al lado. Medido entre
|
|
16120
|
+
// «¿En qué situación está?» y «¿A dónde lo llevamos?» del formulario de solicitud.
|
|
16121
|
+
gridSpan: 2,
|
|
16051
16122
|
uiProps: {
|
|
16052
16123
|
// SPEC-074 — `error` viene del render de CUSTOM (DynamicForm ya lo pasaba) y aquí se
|
|
16053
16124
|
// desechaba: el campo obligatorio más importante era el único mudo.
|
|
@@ -16672,6 +16743,8 @@ const ServicesMap = ({ config, eventBus, apiKey, filter, fill }) => {
|
|
|
16672
16743
|
const { service, url, method, payloadMap } = config.data.list;
|
|
16673
16744
|
void fetchViaEventBus(eventBus, service, url, method ?? "POST", {
|
|
16674
16745
|
...payloadMap,
|
|
16746
|
+
// SPEC-131 T6 — el del config primero, el de la instancia después: quien acota manda.
|
|
16747
|
+
...config.defaultFilter ?? {},
|
|
16675
16748
|
...filter ?? {}
|
|
16676
16749
|
}).then((res) => {
|
|
16677
16750
|
if (!alive) return;
|
|
@@ -18825,6 +18898,10 @@ const useInputFocus = ({
|
|
|
18825
18898
|
};
|
|
18826
18899
|
};
|
|
18827
18900
|
|
|
18901
|
+
function reservaNecesaria(clientHeight, scrollHeight, targetTop) {
|
|
18902
|
+
const debajoDelObjetivo = scrollHeight - targetTop;
|
|
18903
|
+
return Math.max(0, clientHeight - debajoDelObjetivo);
|
|
18904
|
+
}
|
|
18828
18905
|
const useAutoScroll = ({
|
|
18829
18906
|
messagesEndRef,
|
|
18830
18907
|
messagesContainerRef
|
|
@@ -18843,11 +18920,12 @@ const useAutoScroll = ({
|
|
|
18843
18920
|
const container = messagesContainerRef.current;
|
|
18844
18921
|
const spacer = messagesEndRef.current;
|
|
18845
18922
|
if (!container || !spacer || !target) return;
|
|
18846
|
-
spacer.style.minHeight =
|
|
18923
|
+
spacer.style.minHeight = "0px";
|
|
18847
18924
|
const containerRect = container.getBoundingClientRect();
|
|
18848
18925
|
const msgRect = target.getBoundingClientRect();
|
|
18849
18926
|
const padTop = parseFloat(getComputedStyle(container).paddingTop) || 0;
|
|
18850
18927
|
const targetTop = msgRect.top - containerRect.top + container.scrollTop - padTop;
|
|
18928
|
+
spacer.style.minHeight = `${reservaNecesaria(container.clientHeight, container.scrollHeight, targetTop)}px`;
|
|
18851
18929
|
container.scrollTo({ top: targetTop, behavior: "smooth" });
|
|
18852
18930
|
}, [messagesContainerRef, messagesEndRef]);
|
|
18853
18931
|
const last = useCallback((selector) => {
|
|
@@ -18889,15 +18967,33 @@ const useAutoScroll = ({
|
|
|
18889
18967
|
});
|
|
18890
18968
|
}, 150);
|
|
18891
18969
|
}, [doScroll]);
|
|
18892
|
-
const
|
|
18970
|
+
const aterrizarAlFinal = useCallback(() => {
|
|
18971
|
+
const container = messagesContainerRef.current;
|
|
18972
|
+
if (!container) return;
|
|
18973
|
+
resetSpacer();
|
|
18974
|
+
const hasta = Date.now() + 1e3;
|
|
18975
|
+
let ultimoAlto = -1;
|
|
18976
|
+
const pegar = () => {
|
|
18977
|
+
const c = messagesContainerRef.current;
|
|
18978
|
+
if (!c) return;
|
|
18979
|
+
if (c.scrollHeight !== ultimoAlto) {
|
|
18980
|
+
ultimoAlto = c.scrollHeight;
|
|
18981
|
+
c.scrollTop = c.scrollHeight;
|
|
18982
|
+
}
|
|
18983
|
+
if (Date.now() < hasta) requestAnimationFrame(pegar);
|
|
18984
|
+
};
|
|
18985
|
+
requestAnimationFrame(pegar);
|
|
18986
|
+
}, [messagesContainerRef, resetSpacer]);
|
|
18987
|
+
const scrollToBottom = useCallback((suave = true) => {
|
|
18893
18988
|
cancelPending();
|
|
18894
18989
|
resetSpacer();
|
|
18895
18990
|
requestAnimationFrame(() => {
|
|
18991
|
+
const comportamiento = suave ? "smooth" : "auto";
|
|
18896
18992
|
const container = messagesContainerRef.current;
|
|
18897
18993
|
if (container) {
|
|
18898
|
-
container.scrollTo({ top: container.scrollHeight, behavior:
|
|
18994
|
+
container.scrollTo({ top: container.scrollHeight, behavior: comportamiento });
|
|
18899
18995
|
} else {
|
|
18900
|
-
messagesEndRef.current?.scrollIntoView({ behavior:
|
|
18996
|
+
messagesEndRef.current?.scrollIntoView({ behavior: comportamiento, block: "end" });
|
|
18901
18997
|
}
|
|
18902
18998
|
});
|
|
18903
18999
|
}, [cancelPending, resetSpacer, messagesContainerRef, messagesEndRef]);
|
|
@@ -18905,7 +19001,9 @@ const useAutoScroll = ({
|
|
|
18905
19001
|
scrollToLastUserMessage,
|
|
18906
19002
|
pinLastUserMessage,
|
|
18907
19003
|
scrollToProactive,
|
|
18908
|
-
scrollToBottom
|
|
19004
|
+
scrollToBottom,
|
|
19005
|
+
aterrizarAlFinal
|
|
19006
|
+
// SPEC-131 T3
|
|
18909
19007
|
};
|
|
18910
19008
|
};
|
|
18911
19009
|
|
|
@@ -19458,7 +19556,7 @@ const Chat = ({
|
|
|
19458
19556
|
usePreventOverscroll();
|
|
19459
19557
|
const { showThinking, scrollButtonState, scrollToTopPending } = useChatStore();
|
|
19460
19558
|
const { attachments, handleAttachmentsChange, clearAttachments } = useAttachments();
|
|
19461
|
-
const { scrollToBottom, scrollToLastUserMessage, pinLastUserMessage, scrollToProactive } = useAutoScroll({
|
|
19559
|
+
const { scrollToBottom, scrollToLastUserMessage, pinLastUserMessage, scrollToProactive, aterrizarAlFinal } = useAutoScroll({
|
|
19462
19560
|
messagesEndRef,
|
|
19463
19561
|
messagesContainerRef
|
|
19464
19562
|
});
|
|
@@ -19486,11 +19584,23 @@ const Chat = ({
|
|
|
19486
19584
|
}
|
|
19487
19585
|
}, [scrollToTopPending, scrollToLastUserMessage]);
|
|
19488
19586
|
useEffect(() => {
|
|
19587
|
+
if (messages[messages.length - 1]?.proactive) return;
|
|
19489
19588
|
const spacer = messagesEndRef.current;
|
|
19490
19589
|
if (spacer && spacer.style.minHeight && spacer.style.minHeight !== "0px") {
|
|
19491
19590
|
pinLastUserMessage();
|
|
19492
19591
|
}
|
|
19493
19592
|
}, [messages]);
|
|
19593
|
+
const yaAterrizo = useRef(false);
|
|
19594
|
+
useEffect(() => {
|
|
19595
|
+
if (messages.length === 0) {
|
|
19596
|
+
yaAterrizo.current = false;
|
|
19597
|
+
return;
|
|
19598
|
+
}
|
|
19599
|
+
if (yaAterrizo.current) return;
|
|
19600
|
+
yaAterrizo.current = true;
|
|
19601
|
+
if (messages[messages.length - 1]?.proactive) return;
|
|
19602
|
+
aterrizarAlFinal();
|
|
19603
|
+
}, [messages, aterrizarAlFinal]);
|
|
19494
19604
|
const lastProactiveIdRef = useRef(void 0);
|
|
19495
19605
|
useEffect(() => {
|
|
19496
19606
|
const last = messages[messages.length - 1];
|
|
@@ -19608,7 +19718,7 @@ const Chat = ({
|
|
|
19608
19718
|
{
|
|
19609
19719
|
className: `${COMPONENT_PREFIX$1}chat__scroll-to-bottom`,
|
|
19610
19720
|
variant: "circle",
|
|
19611
|
-
onClick: scrollToBottom,
|
|
19721
|
+
onClick: () => scrollToBottom(),
|
|
19612
19722
|
"aria-label": "Ir al final",
|
|
19613
19723
|
icon: /* @__PURE__ */ jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
19614
19724
|
/* @__PURE__ */ jsx("path", { d: "M12 8v8", strokeWidth: "2", strokeLinecap: "round" }),
|
package/dist/index.js
CHANGED
|
@@ -10945,6 +10945,7 @@ const Drawer = ({
|
|
|
10945
10945
|
"data-testid": dataTestId,
|
|
10946
10946
|
scrollIndicatorOffset = 1
|
|
10947
10947
|
}) => {
|
|
10948
|
+
const idTitulo = React.useId();
|
|
10948
10949
|
const [isMobile, setIsMobile] = React.useState(false);
|
|
10949
10950
|
const contentRef = React.useRef(null);
|
|
10950
10951
|
const [showScrollIndicator, setShowScrollIndicator] = React.useState(false);
|
|
@@ -11068,14 +11069,14 @@ const Drawer = ({
|
|
|
11068
11069
|
style: getDrawerStyles(),
|
|
11069
11070
|
role: "dialog",
|
|
11070
11071
|
"aria-modal": "true",
|
|
11071
|
-
"aria-labelledby": title ?
|
|
11072
|
+
"aria-labelledby": title ? idTitulo : void 0,
|
|
11072
11073
|
"data-testid": dataTestId,
|
|
11073
11074
|
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${COMPONENT_PREFIX}drawer__viewport`, children: [
|
|
11074
11075
|
(title || showCloseButton || headerExtra) && /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${COMPONENT_PREFIX}drawer__header`, children: [
|
|
11075
11076
|
title && /* @__PURE__ */ jsxRuntime.jsx(
|
|
11076
11077
|
"h2",
|
|
11077
11078
|
{
|
|
11078
|
-
id:
|
|
11079
|
+
id: idTitulo,
|
|
11079
11080
|
className: `${COMPONENT_PREFIX}drawer__title`,
|
|
11080
11081
|
children: title
|
|
11081
11082
|
}
|
|
@@ -13188,7 +13189,9 @@ function mapFieldsToFormFields(fields, mode) {
|
|
|
13188
13189
|
readOnly: field.readOnly,
|
|
13189
13190
|
placeholder: field.placeholder,
|
|
13190
13191
|
uiProps: field.uiProps,
|
|
13191
|
-
transformOnSubmit: field.parseValue
|
|
13192
|
+
transformOnSubmit: field.parseValue,
|
|
13193
|
+
gridSpan: field.gridSpan
|
|
13194
|
+
// SPEC-131 T7
|
|
13192
13195
|
}));
|
|
13193
13196
|
}
|
|
13194
13197
|
|
|
@@ -13368,6 +13371,47 @@ function calculateTotalPages(totalRecords, limit) {
|
|
|
13368
13371
|
return Math.ceil(totalRecords / limit);
|
|
13369
13372
|
}
|
|
13370
13373
|
|
|
13374
|
+
function etiquetaDeOpcion(valor, uiProps) {
|
|
13375
|
+
const opciones = uiProps?.options;
|
|
13376
|
+
if (!Array.isArray(opciones)) return null;
|
|
13377
|
+
const encontrada = opciones.find((o) => o && String(o.value) === String(valor));
|
|
13378
|
+
return encontrada?.label != null ? String(encontrada.label) : null;
|
|
13379
|
+
}
|
|
13380
|
+
function esNumerico(moleculeType) {
|
|
13381
|
+
return moleculeType === "INPUT_CURRENCY" || moleculeType === "INPUT_NUMBER";
|
|
13382
|
+
}
|
|
13383
|
+
function aTexto(valor, uiProps, moleculeType) {
|
|
13384
|
+
if (valor == null || valor === "") return "";
|
|
13385
|
+
if (typeof valor === "boolean") return valor ? "S\xED" : "No";
|
|
13386
|
+
if (esNumerico(moleculeType)) {
|
|
13387
|
+
const n = Number(String(valor).replace(/[^\d.-]/g, ""));
|
|
13388
|
+
if (Number.isFinite(n)) return n.toLocaleString();
|
|
13389
|
+
}
|
|
13390
|
+
if (typeof File !== "undefined" && valor instanceof File) return valor.name;
|
|
13391
|
+
if (Array.isArray(valor)) {
|
|
13392
|
+
const partes = valor.map((v) => aTexto(v, uiProps, moleculeType)).filter(Boolean);
|
|
13393
|
+
return partes.join(", ");
|
|
13394
|
+
}
|
|
13395
|
+
if (typeof valor === "object") {
|
|
13396
|
+
const o = valor;
|
|
13397
|
+
for (const clave of ["label", "address", "name", "text", "value"]) {
|
|
13398
|
+
if (typeof o[clave] === "string" && o[clave]) return o[clave];
|
|
13399
|
+
}
|
|
13400
|
+
return "";
|
|
13401
|
+
}
|
|
13402
|
+
return etiquetaDeOpcion(valor, uiProps) ?? String(valor);
|
|
13403
|
+
}
|
|
13404
|
+
function resumenParaConfirmar(campos, datos) {
|
|
13405
|
+
if (!datos) return [];
|
|
13406
|
+
const lineas = [];
|
|
13407
|
+
for (const campo of campos) {
|
|
13408
|
+
const valor = aTexto(datos[campo.name], campo.uiProps, campo.moleculeType);
|
|
13409
|
+
if (!valor) continue;
|
|
13410
|
+
lineas.push({ label: campo.label ?? campo.name, valor });
|
|
13411
|
+
}
|
|
13412
|
+
return lineas;
|
|
13413
|
+
}
|
|
13414
|
+
|
|
13371
13415
|
const useAnchoPropio = () => {
|
|
13372
13416
|
const ref = React.useRef(null);
|
|
13373
13417
|
const [ancho, setAncho] = React.useState(null);
|
|
@@ -13812,6 +13856,10 @@ function Crud({
|
|
|
13812
13856
|
return "Formulario";
|
|
13813
13857
|
}
|
|
13814
13858
|
};
|
|
13859
|
+
const resumenConfirmacion = React.useMemo(
|
|
13860
|
+
() => confirmAction === "create" || confirmAction === "edit" ? resumenParaConfirmar(formFields, pendingFormData) : [],
|
|
13861
|
+
[confirmAction, formFields, pendingFormData]
|
|
13862
|
+
);
|
|
13815
13863
|
const getConfirmMessage = () => {
|
|
13816
13864
|
switch (confirmAction) {
|
|
13817
13865
|
case "create":
|
|
@@ -13951,6 +13999,10 @@ function Crud({
|
|
|
13951
13999
|
className: `${COMPONENT_PREFIX}crud__confirm-drawer`,
|
|
13952
14000
|
children: /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${COMPONENT_PREFIX}crud__confirm-content`, children: [
|
|
13953
14001
|
/* @__PURE__ */ jsxRuntime.jsx("p", { className: `${COMPONENT_PREFIX}crud__confirm-message`, children: getConfirmMessage() }),
|
|
14002
|
+
resumenConfirmacion.length > 0 && /* @__PURE__ */ jsxRuntime.jsx("dl", { className: `${COMPONENT_PREFIX}crud__confirm-summary`, children: resumenConfirmacion.map((linea) => /* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${COMPONENT_PREFIX}crud__confirm-summary-row`, children: [
|
|
14003
|
+
/* @__PURE__ */ jsxRuntime.jsx("dt", { children: linea.label }),
|
|
14004
|
+
/* @__PURE__ */ jsxRuntime.jsx("dd", { children: linea.valor })
|
|
14005
|
+
] }, linea.label)) }),
|
|
13954
14006
|
/* @__PURE__ */ jsxRuntime.jsxs("div", { className: `${COMPONENT_PREFIX}crud__confirm-actions`, children: [
|
|
13955
14007
|
/* @__PURE__ */ jsxRuntime.jsx(
|
|
13956
14008
|
Button,
|
|
@@ -15896,7 +15948,8 @@ function usePlaceSuggestions(query, enabled) {
|
|
|
15896
15948
|
return {
|
|
15897
15949
|
lat: typeof loc.lat === "function" ? loc.lat() : loc.lat,
|
|
15898
15950
|
lng: typeof loc.lng === "function" ? loc.lng() : loc.lng,
|
|
15899
|
-
address: String(place.formattedAddress ?? "")
|
|
15951
|
+
address: limpiarDireccion(String(place.formattedAddress ?? ""))
|
|
15952
|
+
// SPEC-131 T6
|
|
15900
15953
|
};
|
|
15901
15954
|
} catch {
|
|
15902
15955
|
return null;
|
|
@@ -15906,13 +15959,27 @@ function usePlaceSuggestions(query, enabled) {
|
|
|
15906
15959
|
);
|
|
15907
15960
|
return { suggestions, resolve, clear: () => setSuggestions([]) };
|
|
15908
15961
|
}
|
|
15962
|
+
function limpiarDireccion(address) {
|
|
15963
|
+
const normal = (s) => s.trim().toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "");
|
|
15964
|
+
const vistos = /* @__PURE__ */ new Set();
|
|
15965
|
+
const tramos = [];
|
|
15966
|
+
for (const tramo of address.split(",")) {
|
|
15967
|
+
const limpio = tramo.trim();
|
|
15968
|
+
if (!limpio) continue;
|
|
15969
|
+
const clave = normal(limpio);
|
|
15970
|
+
if (vistos.has(clave)) continue;
|
|
15971
|
+
vistos.add(clave);
|
|
15972
|
+
tramos.push(limpio);
|
|
15973
|
+
}
|
|
15974
|
+
return tramos.join(", ");
|
|
15975
|
+
}
|
|
15909
15976
|
function reverseGeocode(lat, lng) {
|
|
15910
15977
|
return new Promise((resolve) => {
|
|
15911
15978
|
const maps = window.google?.maps;
|
|
15912
15979
|
if (!maps?.Geocoder) return resolve(null);
|
|
15913
15980
|
new maps.Geocoder().geocode({ location: { lat, lng } }, (results, status) => {
|
|
15914
15981
|
if (status !== "OK" || !results?.length) return resolve(null);
|
|
15915
|
-
resolve(String(results[0].formatted_address ?? "") || null);
|
|
15982
|
+
resolve(limpiarDireccion(String(results[0].formatted_address ?? "")) || null);
|
|
15916
15983
|
});
|
|
15917
15984
|
});
|
|
15918
15985
|
}
|
|
@@ -16069,6 +16136,10 @@ function toRenderField(f, loaded, eventBus, mapsApiKey, loadError) {
|
|
|
16069
16136
|
showInModes: f.showInModes,
|
|
16070
16137
|
required: f.required,
|
|
16071
16138
|
placeholder: f.placeholder,
|
|
16139
|
+
// SPEC-131 T7 — la fila entera: al elegir la dirección se despliega el mapa de verificación
|
|
16140
|
+
// y, a media fila, su campo hermano se quedaba con ~400px de blanco al lado. Medido entre
|
|
16141
|
+
// «¿En qué situación está?» y «¿A dónde lo llevamos?» del formulario de solicitud.
|
|
16142
|
+
gridSpan: 2,
|
|
16072
16143
|
uiProps: {
|
|
16073
16144
|
// SPEC-074 — `error` viene del render de CUSTOM (DynamicForm ya lo pasaba) y aquí se
|
|
16074
16145
|
// desechaba: el campo obligatorio más importante era el único mudo.
|
|
@@ -16693,6 +16764,8 @@ const ServicesMap = ({ config, eventBus, apiKey, filter, fill }) => {
|
|
|
16693
16764
|
const { service, url, method, payloadMap } = config.data.list;
|
|
16694
16765
|
void fetchViaEventBus(eventBus, service, url, method ?? "POST", {
|
|
16695
16766
|
...payloadMap,
|
|
16767
|
+
// SPEC-131 T6 — el del config primero, el de la instancia después: quien acota manda.
|
|
16768
|
+
...config.defaultFilter ?? {},
|
|
16696
16769
|
...filter ?? {}
|
|
16697
16770
|
}).then((res) => {
|
|
16698
16771
|
if (!alive) return;
|
|
@@ -18846,6 +18919,10 @@ const useInputFocus = ({
|
|
|
18846
18919
|
};
|
|
18847
18920
|
};
|
|
18848
18921
|
|
|
18922
|
+
function reservaNecesaria(clientHeight, scrollHeight, targetTop) {
|
|
18923
|
+
const debajoDelObjetivo = scrollHeight - targetTop;
|
|
18924
|
+
return Math.max(0, clientHeight - debajoDelObjetivo);
|
|
18925
|
+
}
|
|
18849
18926
|
const useAutoScroll = ({
|
|
18850
18927
|
messagesEndRef,
|
|
18851
18928
|
messagesContainerRef
|
|
@@ -18864,11 +18941,12 @@ const useAutoScroll = ({
|
|
|
18864
18941
|
const container = messagesContainerRef.current;
|
|
18865
18942
|
const spacer = messagesEndRef.current;
|
|
18866
18943
|
if (!container || !spacer || !target) return;
|
|
18867
|
-
spacer.style.minHeight =
|
|
18944
|
+
spacer.style.minHeight = "0px";
|
|
18868
18945
|
const containerRect = container.getBoundingClientRect();
|
|
18869
18946
|
const msgRect = target.getBoundingClientRect();
|
|
18870
18947
|
const padTop = parseFloat(getComputedStyle(container).paddingTop) || 0;
|
|
18871
18948
|
const targetTop = msgRect.top - containerRect.top + container.scrollTop - padTop;
|
|
18949
|
+
spacer.style.minHeight = `${reservaNecesaria(container.clientHeight, container.scrollHeight, targetTop)}px`;
|
|
18872
18950
|
container.scrollTo({ top: targetTop, behavior: "smooth" });
|
|
18873
18951
|
}, [messagesContainerRef, messagesEndRef]);
|
|
18874
18952
|
const last = React.useCallback((selector) => {
|
|
@@ -18910,15 +18988,33 @@ const useAutoScroll = ({
|
|
|
18910
18988
|
});
|
|
18911
18989
|
}, 150);
|
|
18912
18990
|
}, [doScroll]);
|
|
18913
|
-
const
|
|
18991
|
+
const aterrizarAlFinal = React.useCallback(() => {
|
|
18992
|
+
const container = messagesContainerRef.current;
|
|
18993
|
+
if (!container) return;
|
|
18994
|
+
resetSpacer();
|
|
18995
|
+
const hasta = Date.now() + 1e3;
|
|
18996
|
+
let ultimoAlto = -1;
|
|
18997
|
+
const pegar = () => {
|
|
18998
|
+
const c = messagesContainerRef.current;
|
|
18999
|
+
if (!c) return;
|
|
19000
|
+
if (c.scrollHeight !== ultimoAlto) {
|
|
19001
|
+
ultimoAlto = c.scrollHeight;
|
|
19002
|
+
c.scrollTop = c.scrollHeight;
|
|
19003
|
+
}
|
|
19004
|
+
if (Date.now() < hasta) requestAnimationFrame(pegar);
|
|
19005
|
+
};
|
|
19006
|
+
requestAnimationFrame(pegar);
|
|
19007
|
+
}, [messagesContainerRef, resetSpacer]);
|
|
19008
|
+
const scrollToBottom = React.useCallback((suave = true) => {
|
|
18914
19009
|
cancelPending();
|
|
18915
19010
|
resetSpacer();
|
|
18916
19011
|
requestAnimationFrame(() => {
|
|
19012
|
+
const comportamiento = suave ? "smooth" : "auto";
|
|
18917
19013
|
const container = messagesContainerRef.current;
|
|
18918
19014
|
if (container) {
|
|
18919
|
-
container.scrollTo({ top: container.scrollHeight, behavior:
|
|
19015
|
+
container.scrollTo({ top: container.scrollHeight, behavior: comportamiento });
|
|
18920
19016
|
} else {
|
|
18921
|
-
messagesEndRef.current?.scrollIntoView({ behavior:
|
|
19017
|
+
messagesEndRef.current?.scrollIntoView({ behavior: comportamiento, block: "end" });
|
|
18922
19018
|
}
|
|
18923
19019
|
});
|
|
18924
19020
|
}, [cancelPending, resetSpacer, messagesContainerRef, messagesEndRef]);
|
|
@@ -18926,7 +19022,9 @@ const useAutoScroll = ({
|
|
|
18926
19022
|
scrollToLastUserMessage,
|
|
18927
19023
|
pinLastUserMessage,
|
|
18928
19024
|
scrollToProactive,
|
|
18929
|
-
scrollToBottom
|
|
19025
|
+
scrollToBottom,
|
|
19026
|
+
aterrizarAlFinal
|
|
19027
|
+
// SPEC-131 T3
|
|
18930
19028
|
};
|
|
18931
19029
|
};
|
|
18932
19030
|
|
|
@@ -19479,7 +19577,7 @@ const Chat = ({
|
|
|
19479
19577
|
usePreventOverscroll();
|
|
19480
19578
|
const { showThinking, scrollButtonState, scrollToTopPending } = useChatStore();
|
|
19481
19579
|
const { attachments, handleAttachmentsChange, clearAttachments } = useAttachments();
|
|
19482
|
-
const { scrollToBottom, scrollToLastUserMessage, pinLastUserMessage, scrollToProactive } = useAutoScroll({
|
|
19580
|
+
const { scrollToBottom, scrollToLastUserMessage, pinLastUserMessage, scrollToProactive, aterrizarAlFinal } = useAutoScroll({
|
|
19483
19581
|
messagesEndRef,
|
|
19484
19582
|
messagesContainerRef
|
|
19485
19583
|
});
|
|
@@ -19507,11 +19605,23 @@ const Chat = ({
|
|
|
19507
19605
|
}
|
|
19508
19606
|
}, [scrollToTopPending, scrollToLastUserMessage]);
|
|
19509
19607
|
React.useEffect(() => {
|
|
19608
|
+
if (messages[messages.length - 1]?.proactive) return;
|
|
19510
19609
|
const spacer = messagesEndRef.current;
|
|
19511
19610
|
if (spacer && spacer.style.minHeight && spacer.style.minHeight !== "0px") {
|
|
19512
19611
|
pinLastUserMessage();
|
|
19513
19612
|
}
|
|
19514
19613
|
}, [messages]);
|
|
19614
|
+
const yaAterrizo = React.useRef(false);
|
|
19615
|
+
React.useEffect(() => {
|
|
19616
|
+
if (messages.length === 0) {
|
|
19617
|
+
yaAterrizo.current = false;
|
|
19618
|
+
return;
|
|
19619
|
+
}
|
|
19620
|
+
if (yaAterrizo.current) return;
|
|
19621
|
+
yaAterrizo.current = true;
|
|
19622
|
+
if (messages[messages.length - 1]?.proactive) return;
|
|
19623
|
+
aterrizarAlFinal();
|
|
19624
|
+
}, [messages, aterrizarAlFinal]);
|
|
19515
19625
|
const lastProactiveIdRef = React.useRef(void 0);
|
|
19516
19626
|
React.useEffect(() => {
|
|
19517
19627
|
const last = messages[messages.length - 1];
|
|
@@ -19629,7 +19739,7 @@ const Chat = ({
|
|
|
19629
19739
|
{
|
|
19630
19740
|
className: `${COMPONENT_PREFIX$1}chat__scroll-to-bottom`,
|
|
19631
19741
|
variant: "circle",
|
|
19632
|
-
onClick: scrollToBottom,
|
|
19742
|
+
onClick: () => scrollToBottom(),
|
|
19633
19743
|
"aria-label": "Ir al final",
|
|
19634
19744
|
icon: /* @__PURE__ */ jsxRuntime.jsxs("svg", { xmlns: "http://www.w3.org/2000/svg", width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", children: [
|
|
19635
19745
|
/* @__PURE__ */ jsxRuntime.jsx("path", { d: "M12 8v8", strokeWidth: "2", strokeLinecap: "round" }),
|