@stackcraftmx/calcita 0.25.0 → 0.26.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 (56) hide show
  1. package/README.md +16 -0
  2. package/dist/api/createApiClient.js +5 -0
  3. package/dist/api/createApiClient.js.map +1 -1
  4. package/dist/appShell/AccountMenu.js +15 -23
  5. package/dist/appShell/AccountMenu.js.map +1 -1
  6. package/dist/appShell/AppLayout.js +15 -8
  7. package/dist/appShell/AppLayout.js.map +1 -1
  8. package/dist/appShell/HeaderTools.js +4 -2
  9. package/dist/appShell/HeaderTools.js.map +1 -1
  10. package/dist/appShell/MenuLinks.js +36 -0
  11. package/dist/appShell/MenuLinks.js.map +1 -0
  12. package/dist/appShell/MobileTabBar.js +3 -3
  13. package/dist/appShell/MobileTabBar.js.map +1 -1
  14. package/dist/appShell/SettingsMenu.js +10 -5
  15. package/dist/appShell/SettingsMenu.js.map +1 -1
  16. package/dist/appShell/SideMenu.js +5 -3
  17. package/dist/appShell/SideMenu.js.map +1 -1
  18. package/dist/appShell/SideMenuEntry.js +3 -3
  19. package/dist/appShell/SideMenuEntry.js.map +1 -1
  20. package/dist/appShell/SubmenuActionsMenu.js +3 -4
  21. package/dist/appShell/SubmenuActionsMenu.js.map +1 -1
  22. package/dist/appShell/TopMenu.js +5 -6
  23. package/dist/appShell/TopMenu.js.map +1 -1
  24. package/dist/appShell/Wordmark.js +11 -0
  25. package/dist/appShell/Wordmark.js.map +1 -0
  26. package/dist/appShell/menuItemText.js +4 -0
  27. package/dist/appShell/menuItemText.js.map +1 -1
  28. package/dist/index.js +6 -3
  29. package/dist/navigation/appMenu.js +13 -1
  30. package/dist/navigation/appMenu.js.map +1 -1
  31. package/dist/observability/logger.js +18 -0
  32. package/dist/observability/logger.js.map +1 -1
  33. package/dist/theme/components/MuiAppBar.js +6 -5
  34. package/dist/theme/components/MuiAppBar.js.map +1 -1
  35. package/dist/theme/components/glassComponents.js +32 -31
  36. package/dist/theme/components/glassComponents.js.map +1 -1
  37. package/dist/theme/components/minimalComponents.js +8 -4
  38. package/dist/theme/components/minimalComponents.js.map +1 -1
  39. package/dist/theme/glassSurface.js +36 -38
  40. package/dist/theme/glassSurface.js.map +1 -1
  41. package/dist/theme/lightDarkStyles.js +48 -0
  42. package/dist/theme/lightDarkStyles.js.map +1 -0
  43. package/dist/types/api/createApiClient.d.ts +17 -0
  44. package/dist/types/appShell/AppLayout.d.ts +5 -1
  45. package/dist/types/appShell/MenuLinks.d.ts +17 -0
  46. package/dist/types/appShell/SettingsMenu.d.ts +3 -1
  47. package/dist/types/appShell/SideMenu.d.ts +3 -1
  48. package/dist/types/appShell/Wordmark.d.ts +6 -0
  49. package/dist/types/appShell/menuItemText.d.ts +16 -0
  50. package/dist/types/index.d.ts +3 -3
  51. package/dist/types/navigation/appMenu.d.ts +8 -0
  52. package/dist/types/observability/logger.d.ts +13 -0
  53. package/dist/types/theme/components/MuiAppBar.d.ts +3 -1
  54. package/dist/types/theme/components/glassComponents.d.ts +5 -0
  55. package/dist/types/theme/lightDarkStyles.d.ts +8 -0
  56. package/package.json +3 -2
package/README.md CHANGED
@@ -282,6 +282,9 @@ nombre; las demás, en «Más».
282
282
  Claro u oscuro tiene tres opciones en el menú de la cuenta: Claro, Oscuro y Automático, que sigue lo que diga el equipo y
283
283
  es lo de omisión. El botón de la barra fija claro u oscuro; Automático es la forma de volver a seguir al equipo.
284
284
 
285
+ Una marca que es una palabra larga va en `wordmark` (a 24 px de alto, en lugar del logo cuadrado y el nombre). Una entrada
286
+ del menú puede llevar a otro sistema (`href`) y traer su ícono ya pintado (`icon`) en lugar del componente (`Icon`).
287
+
285
288
  El menú no se escribe en una lista central: cada módulo trae su `menu.js` y `composeMenu` los junta, los ordena y deja
286
289
  solo los que la sesión puede ver (también dentro de los submenús).
287
290
 
@@ -357,6 +360,19 @@ un `<div>` de la página vieja, con su propio `ToolkitProvider`.
357
360
  los de su isla.
358
361
  - **Un backend que contesta a su manera:** `createApiClient({toProblem, mapResponse})` convierte sus errores a RFC 9457
359
362
  (Laravel manda `{message, errors}`) y desenvuelve sus respuestas (`{data: …}`); `onError` recibe cada error en un lugar.
363
+ Una subida con avance (`uploadWithProgress`) regresa la respuesta tal cual: `apiErrorOf(response)` da el mismo `ApiError`.
364
+ - **Su marca y su menú:** un logo ancho (`wordmark`) en lugar del cuadrado y el nombre; entradas del menú con liga a otro
365
+ sistema (`href`) o con un ícono ya pintado (`icon`, por ejemplo de iconify); y las ligas de la cuenta (`accountMenuItems`)
366
+ salen también sin sesión, detrás del engrane («Regresar al sistema»).
367
+ - **Su `sx` gana:** lo que el proyecto pinte con `sx` les gana a los ajustes de Calcita en claro y en oscuro.
368
+ - **Su registro:** `addLogSink` manda cada anotación también a su reporte (Sentry, Datadog, su endpoint).
369
+ - **Si parte el código en archivos** (`manualChunks`): Calcita en el suyo. Junto con lo demás de `node_modules` arma un
370
+ círculo con MUI y la app truena al abrir («reading 'isValidElementType'»). Su FilePond se queda fuera para seguir bajando
371
+ solo donde se usa:
372
+
373
+ ```js
374
+ if (id.includes('@stackcraftmx/calcita') && !id.includes('/filepond')) return 'calcita';
375
+ ```
360
376
 
361
377
  ```jsx
362
378
  const hashNavigation = createHashNavigation(); // una vez, fuera de los componentes
@@ -66,6 +66,10 @@ async function readProblem(response) {
66
66
  return null;
67
67
  }
68
68
  }
69
+ async function apiErrorOf(response, { toProblem = (body) => body } = {}) {
70
+ const problem = toProblemOrNull(toProblem, await readProblem(response), response);
71
+ return new ApiError({ status: response.status, problem, retryAfterSeconds: retryAfterSecondsOf(response.headers.get("retry-after")) });
72
+ }
69
73
  function combinedSignalOf(signal, timeoutMilliseconds) {
70
74
  const signals = [signal, timeoutMilliseconds > 0 ? AbortSignal.timeout(timeoutMilliseconds) : null].filter(Boolean);
71
75
  if (signals.length < 2) {
@@ -249,6 +253,7 @@ function createApiClient({
249
253
  }
250
254
  export {
251
255
  API_RESPONSE_PARSE,
256
+ apiErrorOf,
252
257
  apiUrlOf,
253
258
  createApiClient,
254
259
  retryAfterSecondsOf
@@ -1 +1 @@
1
- {"version":3,"file":"createApiClient.js","sources":["../../src/api/createApiClient.js"],"sourcesContent":["// La petición de la casa: TODO lo que habla con el servidor pasa por un cliente armado aquí, con fetch del navegador (sin\n// axios ni otra librería). Lo que resuelve UNA vez y no en cada pantalla:\n// - Accept-Language con el idioma vigente de ToolkitProvider en CADA petición: el servidor responde title y detail ya\n// traducidos. Cambiar el idioma aplica a la siguiente petición sin recargar.\n// - errores como ApiError (ApiError.js): el problema RFC 9457 del servidor con el mensaje ya traducido; si la\n// respuesta llegó en otro idioma (un error que armó nginx), el texto genérico del paquete. Sin red o se acabó el\n// tiempo: un ApiError con status 0, la misma forma, para que la pantalla no tenga dos formas de error.\n// - cancelar NO es un error: el AbortError se relanza intacto (quien canceló ya sabe por qué).\n// - 401 (la sesión se venció a media consulta): avisa a onUnauthenticated, que normalmente lleva a iniciar sesión. Una\n// petición que espera el 401 (preguntar si hay sesión al arrancar, el login) pasa skipUnauthenticatedHandler.\n// - el aviso «Hay una versión nueva»: cada respuesta trae en un header qué compilación del front está publicada y se\n// le entrega a reportPublishedBuild.\n// - Laravel (Sanctum): con csrf, manda la cookie XSRF-TOKEN en su header (solo al mismo origen) y ante un 419 pide una\n// cookie nueva y repite la petición UNA vez.\n// - cuerpo objeto → JSON; FormData, Blob y URLSearchParams van tal cual (el navegador pone su boundary).\n// - descargas: el Blob y el nombre que mandó el servidor, con avance; un Excel que llegó cortado (el servidor falló a\n// media escritura después de responder 200) no se entrega.\n// - eventos hacia afuera, para lo que cada proyecto haga a su manera: onRequest (sale), onResponse (llegó, con su status\n// y cuánto tardó) y onError (CADA error en un solo lugar: Sentry, un aviso propio, una métrica). Cancelar no es error.\n// Si un evento del proyecto truena, la petición sigue y se anota en el registro.\n// - un backend que ya existe y contesta a su manera, sin cambiarlo: toProblem convierte su error a la forma RFC 9457\n// (Laravel manda {message, errors}) y mapResponse desenvuelve sus respuestas buenas ({data: …}).\n//\n// const api = createApiClient({baseUrl: '/api/v1', onUnauthenticated: () => navigate('/sign-in')});\n// const orders = await api.get('/orders', {params: {page: 2}, signal});\n// await api.post('/orders', {customerId: 7});\n//\n// Se arma uno por proyecto (en su core) y las pantallas lo usan desde su *Api.js.\n//\n// Funciones puras, no renderizan: function (regla de la casa).\nimport {ApiError, API_ERROR_STATUS} from '@/api/ApiError.js';\nimport {XLSX_CONTENT_TYPE, fileNameFromContentDisposition, isCompleteZip} from '@/files.js';\nimport {CSRF_TOKEN_MISMATCH_STATUS} from '@/laravel/csrfRetry.js';\nimport {createLogger} from '@/observability/logger.js';\nimport {reportPublishedBuild} from '@/observability/publishedBuild.js';\nimport {DEFAULT_LANGUAGE, toolkitSettings} from '@/toolkit/toolkitSettings.js';\n\nconst log = createLogger('api');\n\n/** Cómo se lee una respuesta que salió bien. */\nexport const API_RESPONSE_PARSE = Object.freeze({json: 'json', blob: 'blob', response: 'response'});\n\n// Las consultas se cortan a los 60 s; las descargas van sin límite (0): el servidor arma el archivo mientras lo manda y\n// uno grande no cabe en 60 s. No se queda colgada: se cancela desde el panel de descargas.\nconst DEFAULT_TIMEOUT_MILLISECONDS = 60_000;\nconst NO_TIMEOUT = 0;\nconst DEFAULT_PUBLISHED_BUILD_HEADER = 'X-Frontend-Build';\nconst NO_CONTENT_STATUS = 204;\nconst SERVER_ERROR_STATUS = 500;\n\n/**\n * La dirección completa: la base del cliente más el camino, con los parámetros. Un camino que ya trae http(s) va tal cual.\n * Los parámetros vacíos (undefined, null) no viajan; una lista viaja repetida (estado=a&estado=b).\n *\n * @param {string} baseUrl\n * @param {string} path\n * @param {Record<string, unknown>} [params]\n * @returns {string}\n */\nexport function apiUrlOf(baseUrl, path, params) {\n const url = /^https?:/i.test(path) ? path : `${baseUrl}${path}`;\n const query = new URLSearchParams();\n for (const [name, value] of Object.entries(params ?? {})) {\n for (const item of [].concat(value)) {\n if (item !== undefined && item !== null) {\n query.append(name, String(item));\n }\n }\n }\n const queryText = query.toString();\n if (!queryText) {\n return url;\n }\n return `${url}${url.includes('?') ? '&' : '?'}${queryText}`;\n}\n\n/**\n * ¿El cuerpo va tal cual? FormData (archivos), Blob y URLSearchParams los arma el navegador con su propio tipo.\n *\n * @param {unknown} body\n * @returns {boolean}\n */\nfunction isRawBody(body) {\n return (typeof FormData !== 'undefined' && body instanceof FormData)\n || (typeof Blob !== 'undefined' && body instanceof Blob)\n || body instanceof URLSearchParams\n || body instanceof ArrayBuffer;\n}\n\n/**\n * El valor de una cookie del navegador (la XSRF-TOKEN de Laravel), o null.\n *\n * @param {string} cookieName\n * @returns {string|null}\n */\nfunction readCookie(cookieName) {\n if (typeof document === 'undefined') {\n return null;\n }\n const prefix = `${cookieName}=`;\n const cookie = document.cookie.split(';').map((part) => part.trim()).find((part) => part.startsWith(prefix));\n return cookie ? decodeURIComponent(cookie.slice(prefix.length)) : null;\n}\n\n/**\n * ¿La dirección es del mismo origen que la página? El token CSRF solo viaja al servidor propio: mandarlo a otro dominio\n * se lo regalaría.\n *\n * @param {string} url\n * @returns {boolean}\n */\nfunction isSameOrigin(url) {\n if (typeof window === 'undefined' || !/^https?:/i.test(url)) {\n return true;\n }\n return new URL(url).origin === window.location.origin;\n}\n\n/**\n * Retry-After en segundos: el estándar admite segundos o una fecha HTTP.\n *\n * @param {string|null} retryAfterHeader\n * @returns {number|null}\n */\nexport function retryAfterSecondsOf(retryAfterHeader) {\n if (!retryAfterHeader) {\n return null;\n }\n if (/^\\d+$/.test(retryAfterHeader.trim())) {\n return Number(retryAfterHeader.trim());\n }\n const retryAt = Date.parse(retryAfterHeader);\n return Number.isNaN(retryAt) ? null : Math.max(0, Math.ceil((retryAt - Date.now()) / 1000));\n}\n\n/**\n * El cuerpo RFC 9457 de una respuesta de error, o null si no es JSON (un 502 del proxy en HTML).\n *\n * @param {Response} response\n * @returns {Promise<object|null>}\n */\n/**\n * El problema RFC 9457 del cuerpo de error, pasado por el adaptador del proyecto (toProblem). Lo que no sea un objeto\n * cuenta como sin problema (se usa el texto genérico del paquete).\n *\n * @param {(body: any, response: Response) => any} toProblem\n * @param {object|null} body\n * @param {Response} response\n * @returns {object|null}\n */\nfunction toProblemOrNull(toProblem, body, response) {\n const problem = body === null ? null : toProblem(body, response);\n return problem && typeof problem === 'object' && !Array.isArray(problem) ? problem : null;\n}\n\nasync function readProblem(response) {\n try {\n const body = JSON.parse(await response.text());\n return body && typeof body === 'object' && !Array.isArray(body) ? body : null;\n } catch {\n return null;\n }\n}\n\n/**\n * La señal de la petición: la de quien la pidió (para cancelar) y la del tiempo máximo, juntas.\n *\n * @param {AbortSignal|undefined} signal\n * @param {number} timeoutMilliseconds 0 = sin límite\n * @returns {AbortSignal|undefined}\n */\nfunction combinedSignalOf(signal, timeoutMilliseconds) {\n const signals = [signal, timeoutMilliseconds > 0 ? AbortSignal.timeout(timeoutMilliseconds) : null].filter(Boolean);\n if (signals.length < 2) {\n return signals[0];\n }\n return AbortSignal.any(signals);\n}\n\n/**\n * Lee el cuerpo de una descarga avisando cuánto lleva. total viene en null cuando el servidor arma el archivo al vuelo\n * (sin Content-Length).\n *\n * @param {Response} response\n * @param {((progress: {loaded: number, total: number|null}) => void)|undefined} onDownloadProgress\n * @returns {Promise<Blob>}\n */\nasync function readBlobWithProgress(response, onDownloadProgress) {\n if (!onDownloadProgress || !response.body) {\n return response.blob();\n }\n const total = Number(response.headers.get('content-length')) || null;\n const reader = response.body.getReader();\n const chunks = [];\n let loaded = 0;\n for (let part = await reader.read(); !part.done; part = await reader.read()) {\n chunks.push(part.value);\n loaded += part.value.byteLength;\n onDownloadProgress({loaded, total});\n }\n return new Blob(chunks, {type: response.headers.get('content-type') ?? ''});\n}\n\n/**\n * Lo que acepta cada petición.\n *\n * @typedef {object} ApiRequestOptions\n * @property {unknown} [body] objeto → JSON; FormData, Blob o URLSearchParams tal cual\n * @property {Record<string, unknown>} [params] los de la dirección (?page=2)\n * @property {AbortSignal} [signal] para cancelar\n * @property {Record<string, string>} [headers]\n * @property {'json'|'blob'|'response'} [parse] cómo se lee la respuesta (API_RESPONSE_PARSE); por omisión JSON\n * @property {boolean} [skipUnauthenticatedHandler] un 401 que se espera (¿hay sesión?, el login)\n * @property {number} [timeoutMilliseconds] el de esta petición\n */\n\n/**\n * Lo que acepta una descarga.\n *\n * @typedef {object} ApiDownloadOptions\n * @property {Record<string, unknown>} [params]\n * @property {AbortSignal} [signal]\n * @property {(progress: {loaded: number, total: number|null}) => void} [onDownloadProgress]\n * @property {string} [fallbackFileName] si el servidor no manda el nombre\n * @property {number} [timeoutMilliseconds] por omisión sin límite\n */\n\n/**\n * El cliente del proyecto: cada método regresa el JSON de la respuesta (null si vino vacía) o truena con un ApiError.\n *\n * @typedef {object} ApiClient\n * @property {(method: string, path: string, options?: ApiRequestOptions) => Promise<any>} request\n * @property {(path: string, options?: ApiRequestOptions) => Promise<any>} get\n * @property {(path: string, body?: unknown, options?: ApiRequestOptions) => Promise<any>} post\n * @property {(path: string, body?: unknown, options?: ApiRequestOptions) => Promise<any>} put\n * @property {(path: string, body?: unknown, options?: ApiRequestOptions) => Promise<any>} patch\n * @property {(path: string, options?: ApiRequestOptions) => Promise<any>} delete\n * @property {(path: string, options?: ApiDownloadOptions) => Promise<{blob: Blob, fileName: string}>} download\n */\n\n/**\n * Arma el cliente del proyecto.\n *\n * @param {object} [options]\n * @param {string} [options.baseUrl] lo que va antes de cada camino, p. ej. '/api/v1'\n * @param {RequestCredentials} [options.credentials] 'same-origin' (por omisión, la cookie de sesión del mismo dominio) o\n * 'include' (un API en otro dominio con cookies)\n * @param {() => string|null|undefined} [options.getToken] con él cada petición lleva Authorization: Bearer; sin token, va\n * sin el header (el servidor responde 401)\n * @param {Record<string, string>} [options.headers] los que van en todas las peticiones\n * @param {string|null} [options.publishedBuildHeader] el header con la compilación publicada; null para no leerlo\n * @param {{cookieName?: string, headerName?: string, refreshPath?: string}|null} [options.csrf] Laravel Sanctum:\n * {refreshPath: '/auth/csrf-token'}; por omisión la cookie XSRF-TOKEN y el header X-XSRF-TOKEN\n * @param {number} [options.timeoutMilliseconds] el tiempo máximo de una consulta (0 = sin límite); por omisión 60 s\n * @param {(error: ApiError) => void} [options.onUnauthenticated] qué hacer con un 401 (normalmente ir a iniciar sesión)\n * @param {(request: {method: string, url: string}) => void} [options.onRequest] cada petición, al salir\n * @param {(response: {method: string, url: string, status: number, durationMilliseconds: number}) => void} [options.onResponse]\n * cada respuesta, buena o mala, al llegar\n * @param {(error: ApiError, request: {method: string, url: string}) => void} [options.onError] cada error (sin red, 4xx, 5xx,\n * una respuesta que no es JSON), en un solo lugar: Sentry, un aviso propio. Cancelar no es error\n * @param {(body: any, response: Response) => object|null} [options.toProblem] el error de un backend que no contesta en\n * RFC 9457, a esa forma: (body) => ({title: body.message, errors: body.errors}). Por omisión el cuerpo tal cual\n * @param {(body: any, request: {method: string, url: string}) => any} [options.mapResponse] la respuesta buena de un backend\n * que la envuelve: (body) => body.data. Por omisión tal cual\n * @param {typeof fetch} [options.fetch] para un servidor de mentira o para las pruebas; por omisión el del navegador\n * @returns {ApiClient}\n */\nexport function createApiClient({\n baseUrl = '',\n credentials = 'same-origin',\n getToken,\n headers: commonHeaders = {},\n publishedBuildHeader = DEFAULT_PUBLISHED_BUILD_HEADER,\n csrf = null,\n timeoutMilliseconds: defaultTimeoutMilliseconds = DEFAULT_TIMEOUT_MILLISECONDS,\n onUnauthenticated,\n onRequest,\n onResponse,\n onError,\n toProblem = (body) => body,\n mapResponse = (body) => body,\n fetch: fetchImplementation,\n} = {}) {\n // un evento del proyecto que truena no tumba la petición: se anota y se sigue (nunca en silencio)\n const notify = (eventName, handler, ...eventArguments) => {\n try {\n handler?.(...eventArguments);\n } catch (error) {\n log.error(`el evento ${eventName} del proyecto tronó`, {message: error?.message});\n }\n };\n // OJO: se lee al pedir, no al armar el cliente: así un servidor de mentira que se conecta después también sirve\n const fetchOf = () => fetchImplementation ?? globalThis.fetch;\n const csrfCookieName = csrf?.cookieName ?? 'XSRF-TOKEN';\n const csrfHeaderName = csrf?.headerName ?? 'X-XSRF-TOKEN';\n\n /** Los headers de una petición: los comunes, el idioma vigente, el token y el CSRF. */\n const headersOf = (url, body, extraHeaders) => {\n const headers = {\n Accept: 'application/json',\n 'Accept-Language': toolkitSettings().language ?? DEFAULT_LANGUAGE,\n ...commonHeaders,\n ...extraHeaders,\n };\n if (body !== undefined && !isRawBody(body)) {\n headers['Content-Type'] = 'application/json';\n }\n const token = getToken?.();\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n const csrfToken = csrf && isSameOrigin(url) ? readCookie(csrfCookieName) : null;\n if (csrfToken) {\n headers[csrfHeaderName] = csrfToken;\n }\n return headers;\n };\n\n /** La petición al servidor, ya con su respuesta o con el ApiError de «sin respuesta». */\n const send = async (method, url, {body, signal, headers, timeoutMilliseconds}) => {\n const finalHeaders = headersOf(url, body, headers);\n try {\n return await fetchOf()(url, {\n method,\n headers: finalHeaders,\n body: body === undefined || isRawBody(body) ? body : JSON.stringify(body),\n credentials,\n signal: combinedSignalOf(signal, timeoutMilliseconds),\n });\n } catch (error) {\n // cancelar no es un error de negocio: quien canceló ya sabe por qué\n if (signal?.aborted) {\n throw error;\n }\n const reason = error?.name === 'TimeoutError' ? 'se acabó el tiempo' : 'la petición no llegó al servidor';\n log.error(reason, {method, url, cause: error?.name});\n throw new ApiError({status: API_ERROR_STATUS.offline});\n }\n };\n\n /**\n * Cualquier petición.\n *\n * @param {string} method 'GET', 'POST', 'PUT', 'PATCH' o 'DELETE'\n * @param {string} path relativo a baseUrl (o completo con http)\n * @param {object} [options]\n * @param {unknown} [options.body] objeto → JSON; FormData, Blob o URLSearchParams tal cual\n * @param {Record<string, unknown>} [options.params] los de la dirección (?page=2)\n * @param {AbortSignal} [options.signal] para cancelar\n * @param {Record<string, string>} [options.headers]\n * @param {'json'|'blob'|'response'} [options.parse] cómo se lee la respuesta (API_RESPONSE_PARSE); por omisión JSON\n * @param {boolean} [options.skipUnauthenticatedHandler] un 401 que se espera (¿hay sesión?, el login)\n * @param {number} [options.timeoutMilliseconds] el de esta petición\n * @returns {Promise<any>} el JSON (null si vino vacío o 204), el Blob o la Response\n */\n const request = async (method, path, options = {}) => {\n const url = apiUrlOf(baseUrl, path, options.params);\n try {\n return await requestOnce(method, url, options);\n } catch (error) {\n // cada error del servidor (o sin red) sale por onError; cancelar no es error\n if (error instanceof ApiError) {\n notify('onError', onError, error, {method, url});\n }\n throw error;\n }\n };\n\n const requestOnce = async (method, url, {\n body, signal, headers, parse = API_RESPONSE_PARSE.json, skipUnauthenticatedHandler = false,\n timeoutMilliseconds = defaultTimeoutMilliseconds,\n } = {}) => {\n notify('onRequest', onRequest, {method, url});\n const startedAt = Date.now();\n let response = await send(method, url, {body, signal, headers, timeoutMilliseconds});\n\n // 419 de Laravel: la cookie CSRF venció; se pide una nueva y se repite la MISMA petición una sola vez\n if (response.status === CSRF_TOKEN_MISMATCH_STATUS && csrf?.refreshPath) {\n log.info('token CSRF vencido: se renueva y se reintenta', {method, url});\n await send('GET', apiUrlOf(baseUrl, csrf.refreshPath), {signal, timeoutMilliseconds});\n response = await send(method, url, {body, signal, headers, timeoutMilliseconds});\n }\n\n notify('onResponse', onResponse, {method, url, status: response.status, durationMilliseconds: Date.now() - startedAt});\n\n // cada respuesta (también las de error) dice qué compilación del front está publicada\n if (publishedBuildHeader) {\n reportPublishedBuild(response.headers.get(publishedBuildHeader));\n }\n\n if (!response.ok) {\n const problem = toProblemOrNull(toProblem, await readProblem(response), response);\n // un error que armó otro (nginx) puede llegar en otro idioma: ahí se usa el texto genérico del paquete\n const requestedLanguage = headersOf(url, undefined, headers)['Accept-Language'];\n const responseLanguage = response.headers.get('content-language');\n const apiError = new ApiError({\n status: response.status,\n problem,\n translatedByServer: !responseLanguage || responseLanguage.startsWith(requestedLanguage),\n retryAfterSeconds: retryAfterSecondsOf(response.headers.get('retry-after')),\n });\n // los 4xx son parte del flujo (un 422 es un formulario, un 401 al arrancar es «sin sesión»); los 5xx no\n log[response.status >= SERVER_ERROR_STATUS ? 'error' : 'debug'](`${method} ${url} → ${response.status}`, {\n code: apiError.code, requestId: apiError.requestId ?? response.headers.get('x-request-id'),\n });\n if (apiError.isUnauthenticated && !skipUnauthenticatedHandler) {\n onUnauthenticated?.(apiError);\n }\n throw apiError;\n }\n\n if (parse === API_RESPONSE_PARSE.response) {\n return response;\n }\n if (parse === API_RESPONSE_PARSE.blob) {\n return response.blob();\n }\n if (response.status === NO_CONTENT_STATUS) {\n return null;\n }\n // un 200 con el cuerpo vacío pasa de verdad; JSON.parse('') tronaría\n const text = await response.text();\n if (!text) {\n return null;\n }\n let json;\n try {\n json = JSON.parse(text);\n } catch {\n // un 200 que no es JSON: casi siempre la página de la app que respondió en lugar del API (el camino está mal)\n log.error(`${method} ${url} respondió algo que no es JSON`, {contentType: response.headers.get('content-type')});\n throw new ApiError({status: SERVER_ERROR_STATUS});\n }\n return mapResponse(json, {method, url});\n };\n\n /**\n * Descarga un archivo: el Blob y el nombre que mandó el servidor, con avance. Un Excel cortado no se entrega.\n *\n * @param {string} path\n * @param {object} [options]\n * @param {Record<string, unknown>} [options.params]\n * @param {AbortSignal} [options.signal]\n * @param {(progress: {loaded: number, total: number|null}) => void} [options.onDownloadProgress]\n * @param {string} [options.fallbackFileName] si el servidor no manda el nombre\n * @param {number} [options.timeoutMilliseconds] por omisión sin límite\n * @returns {Promise<{blob: Blob, fileName: string}>}\n * @throws {ApiError} status 500 si un Excel llegó incompleto\n */\n const download = async (path, {params, signal, onDownloadProgress, fallbackFileName = 'descarga', timeoutMilliseconds = NO_TIMEOUT} = {}) => {\n const response = await request('GET', path, {params, signal, parse: API_RESPONSE_PARSE.response, timeoutMilliseconds});\n const blob = await readBlobWithProgress(response, onDownloadProgress);\n const fileName = fileNameFromContentDisposition(response.headers.get('content-disposition'), fallbackFileName);\n // el tipo puede traer parámetros después del «;» (charset): solo cuenta lo de antes\n const contentType = String(response.headers.get('content-type') ?? '').split(';')[0].trim();\n if (contentType === XLSX_CONTENT_TYPE && !(await isCompleteZip(blob))) {\n const requestId = response.headers.get('x-request-id');\n log.error(`GET ${path} → el Excel llegó incompleto: el servidor falló a media escritura`, {requestId, fileName});\n throw new ApiError({status: SERVER_ERROR_STATUS, problem: {request_id: requestId}});\n }\n return {blob, fileName};\n };\n\n return {\n request,\n get: (path, options) => request('GET', path, options),\n post: (path, body, options) => request('POST', path, {...options, body}),\n put: (path, body, options) => request('PUT', path, {...options, body}),\n patch: (path, body, options) => request('PATCH', path, {...options, body}),\n delete: (path, options) => request('DELETE', path, options),\n download,\n };\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"createApiClient.js","sources":["../../src/api/createApiClient.js"],"sourcesContent":["// La petición de la casa: TODO lo que habla con el servidor pasa por un cliente armado aquí, con fetch del navegador (sin\n// axios ni otra librería). Lo que resuelve UNA vez y no en cada pantalla:\n// - Accept-Language con el idioma vigente de ToolkitProvider en CADA petición: el servidor responde title y detail ya\n// traducidos. Cambiar el idioma aplica a la siguiente petición sin recargar.\n// - errores como ApiError (ApiError.js): el problema RFC 9457 del servidor con el mensaje ya traducido; si la\n// respuesta llegó en otro idioma (un error que armó nginx), el texto genérico del paquete. Sin red o se acabó el\n// tiempo: un ApiError con status 0, la misma forma, para que la pantalla no tenga dos formas de error.\n// - cancelar NO es un error: el AbortError se relanza intacto (quien canceló ya sabe por qué).\n// - 401 (la sesión se venció a media consulta): avisa a onUnauthenticated, que normalmente lleva a iniciar sesión. Una\n// petición que espera el 401 (preguntar si hay sesión al arrancar, el login) pasa skipUnauthenticatedHandler.\n// - el aviso «Hay una versión nueva»: cada respuesta trae en un header qué compilación del front está publicada y se\n// le entrega a reportPublishedBuild.\n// - Laravel (Sanctum): con csrf, manda la cookie XSRF-TOKEN en su header (solo al mismo origen) y ante un 419 pide una\n// cookie nueva y repite la petición UNA vez.\n// - cuerpo objeto → JSON; FormData, Blob y URLSearchParams van tal cual (el navegador pone su boundary).\n// - descargas: el Blob y el nombre que mandó el servidor, con avance; un Excel que llegó cortado (el servidor falló a\n// media escritura después de responder 200) no se entrega.\n// - eventos hacia afuera, para lo que cada proyecto haga a su manera: onRequest (sale), onResponse (llegó, con su status\n// y cuánto tardó) y onError (CADA error en un solo lugar: Sentry, un aviso propio, una métrica). Cancelar no es error.\n// Si un evento del proyecto truena, la petición sigue y se anota en el registro.\n// - un backend que ya existe y contesta a su manera, sin cambiarlo: toProblem convierte su error a la forma RFC 9457\n// (Laravel manda {message, errors}) y mapResponse desenvuelve sus respuestas buenas ({data: …}).\n//\n// const api = createApiClient({baseUrl: '/api/v1', onUnauthenticated: () => navigate('/sign-in')});\n// const orders = await api.get('/orders', {params: {page: 2}, signal});\n// await api.post('/orders', {customerId: 7});\n//\n// Se arma uno por proyecto (en su core) y las pantallas lo usan desde su *Api.js.\n//\n// Funciones puras, no renderizan: function (regla de la casa).\nimport {ApiError, API_ERROR_STATUS} from '@/api/ApiError.js';\nimport {XLSX_CONTENT_TYPE, fileNameFromContentDisposition, isCompleteZip} from '@/files.js';\nimport {CSRF_TOKEN_MISMATCH_STATUS} from '@/laravel/csrfRetry.js';\nimport {createLogger} from '@/observability/logger.js';\nimport {reportPublishedBuild} from '@/observability/publishedBuild.js';\nimport {DEFAULT_LANGUAGE, toolkitSettings} from '@/toolkit/toolkitSettings.js';\n\nconst log = createLogger('api');\n\n/** Cómo se lee una respuesta que salió bien. */\nexport const API_RESPONSE_PARSE = Object.freeze({json: 'json', blob: 'blob', response: 'response'});\n\n// Las consultas se cortan a los 60 s; las descargas van sin límite (0): el servidor arma el archivo mientras lo manda y\n// uno grande no cabe en 60 s. No se queda colgada: se cancela desde el panel de descargas.\nconst DEFAULT_TIMEOUT_MILLISECONDS = 60_000;\nconst NO_TIMEOUT = 0;\nconst DEFAULT_PUBLISHED_BUILD_HEADER = 'X-Frontend-Build';\nconst NO_CONTENT_STATUS = 204;\nconst SERVER_ERROR_STATUS = 500;\n\n/**\n * La dirección completa: la base del cliente más el camino, con los parámetros. Un camino que ya trae http(s) va tal cual.\n * Los parámetros vacíos (undefined, null) no viajan; una lista viaja repetida (estado=a&estado=b).\n *\n * @param {string} baseUrl\n * @param {string} path\n * @param {Record<string, unknown>} [params]\n * @returns {string}\n */\nexport function apiUrlOf(baseUrl, path, params) {\n const url = /^https?:/i.test(path) ? path : `${baseUrl}${path}`;\n const query = new URLSearchParams();\n for (const [name, value] of Object.entries(params ?? {})) {\n for (const item of [].concat(value)) {\n if (item !== undefined && item !== null) {\n query.append(name, String(item));\n }\n }\n }\n const queryText = query.toString();\n if (!queryText) {\n return url;\n }\n return `${url}${url.includes('?') ? '&' : '?'}${queryText}`;\n}\n\n/**\n * ¿El cuerpo va tal cual? FormData (archivos), Blob y URLSearchParams los arma el navegador con su propio tipo.\n *\n * @param {unknown} body\n * @returns {boolean}\n */\nfunction isRawBody(body) {\n return (typeof FormData !== 'undefined' && body instanceof FormData)\n || (typeof Blob !== 'undefined' && body instanceof Blob)\n || body instanceof URLSearchParams\n || body instanceof ArrayBuffer;\n}\n\n/**\n * El valor de una cookie del navegador (la XSRF-TOKEN de Laravel), o null.\n *\n * @param {string} cookieName\n * @returns {string|null}\n */\nfunction readCookie(cookieName) {\n if (typeof document === 'undefined') {\n return null;\n }\n const prefix = `${cookieName}=`;\n const cookie = document.cookie.split(';').map((part) => part.trim()).find((part) => part.startsWith(prefix));\n return cookie ? decodeURIComponent(cookie.slice(prefix.length)) : null;\n}\n\n/**\n * ¿La dirección es del mismo origen que la página? El token CSRF solo viaja al servidor propio: mandarlo a otro dominio\n * se lo regalaría.\n *\n * @param {string} url\n * @returns {boolean}\n */\nfunction isSameOrigin(url) {\n if (typeof window === 'undefined' || !/^https?:/i.test(url)) {\n return true;\n }\n return new URL(url).origin === window.location.origin;\n}\n\n/**\n * Retry-After en segundos: el estándar admite segundos o una fecha HTTP.\n *\n * @param {string|null} retryAfterHeader\n * @returns {number|null}\n */\nexport function retryAfterSecondsOf(retryAfterHeader) {\n if (!retryAfterHeader) {\n return null;\n }\n if (/^\\d+$/.test(retryAfterHeader.trim())) {\n return Number(retryAfterHeader.trim());\n }\n const retryAt = Date.parse(retryAfterHeader);\n return Number.isNaN(retryAt) ? null : Math.max(0, Math.ceil((retryAt - Date.now()) / 1000));\n}\n\n/**\n * El cuerpo RFC 9457 de una respuesta de error, o null si no es JSON (un 502 del proxy en HTML).\n *\n * @param {Response} response\n * @returns {Promise<object|null>}\n */\n/**\n * El problema RFC 9457 del cuerpo de error, pasado por el adaptador del proyecto (toProblem). Lo que no sea un objeto\n * cuenta como sin problema (se usa el texto genérico del paquete).\n *\n * @param {(body: any, response: Response) => any} toProblem\n * @param {object|null} body\n * @param {Response} response\n * @returns {object|null}\n */\nfunction toProblemOrNull(toProblem, body, response) {\n const problem = body === null ? null : toProblem(body, response);\n return problem && typeof problem === 'object' && !Array.isArray(problem) ? problem : null;\n}\n\nasync function readProblem(response) {\n try {\n const body = JSON.parse(await response.text());\n return body && typeof body === 'object' && !Array.isArray(body) ? body : null;\n } catch {\n return null;\n }\n}\n\n/**\n * El ApiError de una respuesta que salió mal, para lo que no pasa por createApiClient: la subida con avance\n * (uploadWithProgress regresa la respuesta tal cual) o un fetch propio. Es el mismo error que da el cliente: el problema RFC\n * 9457 del servidor ({title, detail, errors}), su status (401 = sesión vencida, 422 = errores por campo) y el texto del\n * paquete si el cuerpo no es JSON (un 502 del proxy). Así la pantalla no lee el error a mano.\n *\n * const response = await uploadWithProgress(url, form, {headers, onProgress, signal});\n * if (!response.ok) throw await apiErrorOf(response);\n *\n * @param {Response} response\n * @param {{toProblem?: (body: any, response: Response) => any}} [options] el mismo adaptador de createApiClient, para un\n * backend que contesta a su manera (Laravel manda {message, errors})\n * @returns {Promise<ApiError>}\n */\nexport async function apiErrorOf(response, {toProblem = (body) => body} = {}) {\n const problem = toProblemOrNull(toProblem, await readProblem(response), response);\n return new ApiError({status: response.status, problem, retryAfterSeconds: retryAfterSecondsOf(response.headers.get('retry-after'))});\n}\n\n/**\n * La señal de la petición: la de quien la pidió (para cancelar) y la del tiempo máximo, juntas.\n *\n * @param {AbortSignal|undefined} signal\n * @param {number} timeoutMilliseconds 0 = sin límite\n * @returns {AbortSignal|undefined}\n */\nfunction combinedSignalOf(signal, timeoutMilliseconds) {\n const signals = [signal, timeoutMilliseconds > 0 ? AbortSignal.timeout(timeoutMilliseconds) : null].filter(Boolean);\n if (signals.length < 2) {\n return signals[0];\n }\n return AbortSignal.any(signals);\n}\n\n/**\n * Lee el cuerpo de una descarga avisando cuánto lleva. total viene en null cuando el servidor arma el archivo al vuelo\n * (sin Content-Length).\n *\n * @param {Response} response\n * @param {((progress: {loaded: number, total: number|null}) => void)|undefined} onDownloadProgress\n * @returns {Promise<Blob>}\n */\nasync function readBlobWithProgress(response, onDownloadProgress) {\n if (!onDownloadProgress || !response.body) {\n return response.blob();\n }\n const total = Number(response.headers.get('content-length')) || null;\n const reader = response.body.getReader();\n const chunks = [];\n let loaded = 0;\n for (let part = await reader.read(); !part.done; part = await reader.read()) {\n chunks.push(part.value);\n loaded += part.value.byteLength;\n onDownloadProgress({loaded, total});\n }\n return new Blob(chunks, {type: response.headers.get('content-type') ?? ''});\n}\n\n/**\n * Lo que acepta cada petición.\n *\n * @typedef {object} ApiRequestOptions\n * @property {unknown} [body] objeto → JSON; FormData, Blob o URLSearchParams tal cual\n * @property {Record<string, unknown>} [params] los de la dirección (?page=2)\n * @property {AbortSignal} [signal] para cancelar\n * @property {Record<string, string>} [headers]\n * @property {'json'|'blob'|'response'} [parse] cómo se lee la respuesta (API_RESPONSE_PARSE); por omisión JSON\n * @property {boolean} [skipUnauthenticatedHandler] un 401 que se espera (¿hay sesión?, el login)\n * @property {number} [timeoutMilliseconds] el de esta petición\n */\n\n/**\n * Lo que acepta una descarga.\n *\n * @typedef {object} ApiDownloadOptions\n * @property {Record<string, unknown>} [params]\n * @property {AbortSignal} [signal]\n * @property {(progress: {loaded: number, total: number|null}) => void} [onDownloadProgress]\n * @property {string} [fallbackFileName] si el servidor no manda el nombre\n * @property {number} [timeoutMilliseconds] por omisión sin límite\n */\n\n/**\n * El cliente del proyecto: cada método regresa el JSON de la respuesta (null si vino vacía) o truena con un ApiError.\n *\n * @typedef {object} ApiClient\n * @property {(method: string, path: string, options?: ApiRequestOptions) => Promise<any>} request\n * @property {(path: string, options?: ApiRequestOptions) => Promise<any>} get\n * @property {(path: string, body?: unknown, options?: ApiRequestOptions) => Promise<any>} post\n * @property {(path: string, body?: unknown, options?: ApiRequestOptions) => Promise<any>} put\n * @property {(path: string, body?: unknown, options?: ApiRequestOptions) => Promise<any>} patch\n * @property {(path: string, options?: ApiRequestOptions) => Promise<any>} delete\n * @property {(path: string, options?: ApiDownloadOptions) => Promise<{blob: Blob, fileName: string}>} download\n */\n\n/**\n * Arma el cliente del proyecto.\n *\n * @param {object} [options]\n * @param {string} [options.baseUrl] lo que va antes de cada camino, p. ej. '/api/v1'\n * @param {RequestCredentials} [options.credentials] 'same-origin' (por omisión, la cookie de sesión del mismo dominio) o\n * 'include' (un API en otro dominio con cookies)\n * @param {() => string|null|undefined} [options.getToken] con él cada petición lleva Authorization: Bearer; sin token, va\n * sin el header (el servidor responde 401)\n * @param {Record<string, string>} [options.headers] los que van en todas las peticiones\n * @param {string|null} [options.publishedBuildHeader] el header con la compilación publicada; null para no leerlo\n * @param {{cookieName?: string, headerName?: string, refreshPath?: string}|null} [options.csrf] Laravel Sanctum:\n * {refreshPath: '/auth/csrf-token'}; por omisión la cookie XSRF-TOKEN y el header X-XSRF-TOKEN\n * @param {number} [options.timeoutMilliseconds] el tiempo máximo de una consulta (0 = sin límite); por omisión 60 s\n * @param {(error: ApiError) => void} [options.onUnauthenticated] qué hacer con un 401 (normalmente ir a iniciar sesión)\n * @param {(request: {method: string, url: string}) => void} [options.onRequest] cada petición, al salir\n * @param {(response: {method: string, url: string, status: number, durationMilliseconds: number}) => void} [options.onResponse]\n * cada respuesta, buena o mala, al llegar\n * @param {(error: ApiError, request: {method: string, url: string}) => void} [options.onError] cada error (sin red, 4xx, 5xx,\n * una respuesta que no es JSON), en un solo lugar: Sentry, un aviso propio. Cancelar no es error\n * @param {(body: any, response: Response) => object|null} [options.toProblem] el error de un backend que no contesta en\n * RFC 9457, a esa forma: (body) => ({title: body.message, errors: body.errors}). Por omisión el cuerpo tal cual\n * @param {(body: any, request: {method: string, url: string}) => any} [options.mapResponse] la respuesta buena de un backend\n * que la envuelve: (body) => body.data. Por omisión tal cual\n * @param {typeof fetch} [options.fetch] para un servidor de mentira o para las pruebas; por omisión el del navegador\n * @returns {ApiClient}\n */\nexport function createApiClient({\n baseUrl = '',\n credentials = 'same-origin',\n getToken,\n headers: commonHeaders = {},\n publishedBuildHeader = DEFAULT_PUBLISHED_BUILD_HEADER,\n csrf = null,\n timeoutMilliseconds: defaultTimeoutMilliseconds = DEFAULT_TIMEOUT_MILLISECONDS,\n onUnauthenticated,\n onRequest,\n onResponse,\n onError,\n toProblem = (body) => body,\n mapResponse = (body) => body,\n fetch: fetchImplementation,\n} = {}) {\n // un evento del proyecto que truena no tumba la petición: se anota y se sigue (nunca en silencio)\n const notify = (eventName, handler, ...eventArguments) => {\n try {\n handler?.(...eventArguments);\n } catch (error) {\n log.error(`el evento ${eventName} del proyecto tronó`, {message: error?.message});\n }\n };\n // OJO: se lee al pedir, no al armar el cliente: así un servidor de mentira que se conecta después también sirve\n const fetchOf = () => fetchImplementation ?? globalThis.fetch;\n const csrfCookieName = csrf?.cookieName ?? 'XSRF-TOKEN';\n const csrfHeaderName = csrf?.headerName ?? 'X-XSRF-TOKEN';\n\n /** Los headers de una petición: los comunes, el idioma vigente, el token y el CSRF. */\n const headersOf = (url, body, extraHeaders) => {\n const headers = {\n Accept: 'application/json',\n 'Accept-Language': toolkitSettings().language ?? DEFAULT_LANGUAGE,\n ...commonHeaders,\n ...extraHeaders,\n };\n if (body !== undefined && !isRawBody(body)) {\n headers['Content-Type'] = 'application/json';\n }\n const token = getToken?.();\n if (token) {\n headers.Authorization = `Bearer ${token}`;\n }\n const csrfToken = csrf && isSameOrigin(url) ? readCookie(csrfCookieName) : null;\n if (csrfToken) {\n headers[csrfHeaderName] = csrfToken;\n }\n return headers;\n };\n\n /** La petición al servidor, ya con su respuesta o con el ApiError de «sin respuesta». */\n const send = async (method, url, {body, signal, headers, timeoutMilliseconds}) => {\n const finalHeaders = headersOf(url, body, headers);\n try {\n return await fetchOf()(url, {\n method,\n headers: finalHeaders,\n body: body === undefined || isRawBody(body) ? body : JSON.stringify(body),\n credentials,\n signal: combinedSignalOf(signal, timeoutMilliseconds),\n });\n } catch (error) {\n // cancelar no es un error de negocio: quien canceló ya sabe por qué\n if (signal?.aborted) {\n throw error;\n }\n const reason = error?.name === 'TimeoutError' ? 'se acabó el tiempo' : 'la petición no llegó al servidor';\n log.error(reason, {method, url, cause: error?.name});\n throw new ApiError({status: API_ERROR_STATUS.offline});\n }\n };\n\n /**\n * Cualquier petición.\n *\n * @param {string} method 'GET', 'POST', 'PUT', 'PATCH' o 'DELETE'\n * @param {string} path relativo a baseUrl (o completo con http)\n * @param {object} [options]\n * @param {unknown} [options.body] objeto → JSON; FormData, Blob o URLSearchParams tal cual\n * @param {Record<string, unknown>} [options.params] los de la dirección (?page=2)\n * @param {AbortSignal} [options.signal] para cancelar\n * @param {Record<string, string>} [options.headers]\n * @param {'json'|'blob'|'response'} [options.parse] cómo se lee la respuesta (API_RESPONSE_PARSE); por omisión JSON\n * @param {boolean} [options.skipUnauthenticatedHandler] un 401 que se espera (¿hay sesión?, el login)\n * @param {number} [options.timeoutMilliseconds] el de esta petición\n * @returns {Promise<any>} el JSON (null si vino vacío o 204), el Blob o la Response\n */\n const request = async (method, path, options = {}) => {\n const url = apiUrlOf(baseUrl, path, options.params);\n try {\n return await requestOnce(method, url, options);\n } catch (error) {\n // cada error del servidor (o sin red) sale por onError; cancelar no es error\n if (error instanceof ApiError) {\n notify('onError', onError, error, {method, url});\n }\n throw error;\n }\n };\n\n const requestOnce = async (method, url, {\n body, signal, headers, parse = API_RESPONSE_PARSE.json, skipUnauthenticatedHandler = false,\n timeoutMilliseconds = defaultTimeoutMilliseconds,\n } = {}) => {\n notify('onRequest', onRequest, {method, url});\n const startedAt = Date.now();\n let response = await send(method, url, {body, signal, headers, timeoutMilliseconds});\n\n // 419 de Laravel: la cookie CSRF venció; se pide una nueva y se repite la MISMA petición una sola vez\n if (response.status === CSRF_TOKEN_MISMATCH_STATUS && csrf?.refreshPath) {\n log.info('token CSRF vencido: se renueva y se reintenta', {method, url});\n await send('GET', apiUrlOf(baseUrl, csrf.refreshPath), {signal, timeoutMilliseconds});\n response = await send(method, url, {body, signal, headers, timeoutMilliseconds});\n }\n\n notify('onResponse', onResponse, {method, url, status: response.status, durationMilliseconds: Date.now() - startedAt});\n\n // cada respuesta (también las de error) dice qué compilación del front está publicada\n if (publishedBuildHeader) {\n reportPublishedBuild(response.headers.get(publishedBuildHeader));\n }\n\n if (!response.ok) {\n const problem = toProblemOrNull(toProblem, await readProblem(response), response);\n // un error que armó otro (nginx) puede llegar en otro idioma: ahí se usa el texto genérico del paquete\n const requestedLanguage = headersOf(url, undefined, headers)['Accept-Language'];\n const responseLanguage = response.headers.get('content-language');\n const apiError = new ApiError({\n status: response.status,\n problem,\n translatedByServer: !responseLanguage || responseLanguage.startsWith(requestedLanguage),\n retryAfterSeconds: retryAfterSecondsOf(response.headers.get('retry-after')),\n });\n // los 4xx son parte del flujo (un 422 es un formulario, un 401 al arrancar es «sin sesión»); los 5xx no\n log[response.status >= SERVER_ERROR_STATUS ? 'error' : 'debug'](`${method} ${url} → ${response.status}`, {\n code: apiError.code, requestId: apiError.requestId ?? response.headers.get('x-request-id'),\n });\n if (apiError.isUnauthenticated && !skipUnauthenticatedHandler) {\n onUnauthenticated?.(apiError);\n }\n throw apiError;\n }\n\n if (parse === API_RESPONSE_PARSE.response) {\n return response;\n }\n if (parse === API_RESPONSE_PARSE.blob) {\n return response.blob();\n }\n if (response.status === NO_CONTENT_STATUS) {\n return null;\n }\n // un 200 con el cuerpo vacío pasa de verdad; JSON.parse('') tronaría\n const text = await response.text();\n if (!text) {\n return null;\n }\n let json;\n try {\n json = JSON.parse(text);\n } catch {\n // un 200 que no es JSON: casi siempre la página de la app que respondió en lugar del API (el camino está mal)\n log.error(`${method} ${url} respondió algo que no es JSON`, {contentType: response.headers.get('content-type')});\n throw new ApiError({status: SERVER_ERROR_STATUS});\n }\n return mapResponse(json, {method, url});\n };\n\n /**\n * Descarga un archivo: el Blob y el nombre que mandó el servidor, con avance. Un Excel cortado no se entrega.\n *\n * @param {string} path\n * @param {object} [options]\n * @param {Record<string, unknown>} [options.params]\n * @param {AbortSignal} [options.signal]\n * @param {(progress: {loaded: number, total: number|null}) => void} [options.onDownloadProgress]\n * @param {string} [options.fallbackFileName] si el servidor no manda el nombre\n * @param {number} [options.timeoutMilliseconds] por omisión sin límite\n * @returns {Promise<{blob: Blob, fileName: string}>}\n * @throws {ApiError} status 500 si un Excel llegó incompleto\n */\n const download = async (path, {params, signal, onDownloadProgress, fallbackFileName = 'descarga', timeoutMilliseconds = NO_TIMEOUT} = {}) => {\n const response = await request('GET', path, {params, signal, parse: API_RESPONSE_PARSE.response, timeoutMilliseconds});\n const blob = await readBlobWithProgress(response, onDownloadProgress);\n const fileName = fileNameFromContentDisposition(response.headers.get('content-disposition'), fallbackFileName);\n // el tipo puede traer parámetros después del «;» (charset): solo cuenta lo de antes\n const contentType = String(response.headers.get('content-type') ?? '').split(';')[0].trim();\n if (contentType === XLSX_CONTENT_TYPE && !(await isCompleteZip(blob))) {\n const requestId = response.headers.get('x-request-id');\n log.error(`GET ${path} → el Excel llegó incompleto: el servidor falló a media escritura`, {requestId, fileName});\n throw new ApiError({status: SERVER_ERROR_STATUS, problem: {request_id: requestId}});\n }\n return {blob, fileName};\n };\n\n return {\n request,\n get: (path, options) => request('GET', path, options),\n post: (path, body, options) => request('POST', path, {...options, body}),\n put: (path, body, options) => request('PUT', path, {...options, body}),\n patch: (path, body, options) => request('PATCH', path, {...options, body}),\n delete: (path, options) => request('DELETE', path, options),\n download,\n };\n}\n"],"names":[],"mappings":";;;;;;AAqCA,MAAM,MAAM,aAAa,KAAK;AAGlB,MAAC,qBAAqB,OAAO,OAAO,EAAC,MAAM,QAAQ,MAAM,QAAQ,UAAU,WAAU,CAAC;AAIlG,MAAM,+BAA+B;AACrC,MAAM,aAAa;AACnB,MAAM,iCAAiC;AACvC,MAAM,oBAAoB;AAC1B,MAAM,sBAAsB;AAWrB,SAAS,SAAS,SAAS,MAAM,QAAQ;AAC5C,QAAM,MAAM,YAAY,KAAK,IAAI,IAAI,OAAO,GAAG,OAAO,GAAG,IAAI;AAC7D,QAAM,QAAQ,IAAI,gBAAe;AACjC,aAAW,CAAC,MAAM,KAAK,KAAK,OAAO,QAAQ,UAAU,CAAA,CAAE,GAAG;AACtD,eAAW,QAAQ,CAAA,EAAG,OAAO,KAAK,GAAG;AACjC,UAAI,SAAS,UAAa,SAAS,MAAM;AACrC,cAAM,OAAO,MAAM,OAAO,IAAI,CAAC;AAAA,MACnC;AAAA,IACJ;AAAA,EACJ;AACA,QAAM,YAAY,MAAM,SAAQ;AAChC,MAAI,CAAC,WAAW;AACZ,WAAO;AAAA,EACX;AACA,SAAO,GAAG,GAAG,GAAG,IAAI,SAAS,GAAG,IAAI,MAAM,GAAG,GAAG,SAAS;AAC7D;AAQA,SAAS,UAAU,MAAM;AACrB,SAAQ,OAAO,aAAa,eAAe,gBAAgB,YACnD,OAAO,SAAS,eAAe,gBAAgB,QAChD,gBAAgB,mBAChB,gBAAgB;AAC3B;AAQA,SAAS,WAAW,YAAY;AAC5B,MAAI,OAAO,aAAa,aAAa;AACjC,WAAO;AAAA,EACX;AACA,QAAM,SAAS,GAAG,UAAU;AAC5B,QAAM,SAAS,SAAS,OAAO,MAAM,GAAG,EAAE,IAAI,CAAC,SAAS,KAAK,KAAI,CAAE,EAAE,KAAK,CAAC,SAAS,KAAK,WAAW,MAAM,CAAC;AAC3G,SAAO,SAAS,mBAAmB,OAAO,MAAM,OAAO,MAAM,CAAC,IAAI;AACtE;AASA,SAAS,aAAa,KAAK;AACvB,MAAI,OAAO,WAAW,eAAe,CAAC,YAAY,KAAK,GAAG,GAAG;AACzD,WAAO;AAAA,EACX;AACA,SAAO,IAAI,IAAI,GAAG,EAAE,WAAW,OAAO,SAAS;AACnD;AAQO,SAAS,oBAAoB,kBAAkB;AAClD,MAAI,CAAC,kBAAkB;AACnB,WAAO;AAAA,EACX;AACA,MAAI,QAAQ,KAAK,iBAAiB,KAAI,CAAE,GAAG;AACvC,WAAO,OAAO,iBAAiB,MAAM;AAAA,EACzC;AACA,QAAM,UAAU,KAAK,MAAM,gBAAgB;AAC3C,SAAO,OAAO,MAAM,OAAO,IAAI,OAAO,KAAK,IAAI,GAAG,KAAK,MAAM,UAAU,KAAK,IAAG,KAAM,GAAI,CAAC;AAC9F;AAiBA,SAAS,gBAAgB,WAAW,MAAM,UAAU;AAChD,QAAM,UAAU,SAAS,OAAO,OAAO,UAAU,MAAM,QAAQ;AAC/D,SAAO,WAAW,OAAO,YAAY,YAAY,CAAC,MAAM,QAAQ,OAAO,IAAI,UAAU;AACzF;AAEA,eAAe,YAAY,UAAU;AACjC,MAAI;AACA,UAAM,OAAO,KAAK,MAAM,MAAM,SAAS,KAAI,CAAE;AAC7C,WAAO,QAAQ,OAAO,SAAS,YAAY,CAAC,MAAM,QAAQ,IAAI,IAAI,OAAO;AAAA,EAC7E,QAAQ;AACJ,WAAO;AAAA,EACX;AACJ;AAgBO,eAAe,WAAW,UAAU,EAAC,YAAY,CAAC,SAAS,KAAI,IAAI,IAAI;AAC1E,QAAM,UAAU,gBAAgB,WAAW,MAAM,YAAY,QAAQ,GAAG,QAAQ;AAChF,SAAO,IAAI,SAAS,EAAC,QAAQ,SAAS,QAAQ,SAAS,mBAAmB,oBAAoB,SAAS,QAAQ,IAAI,aAAa,CAAC,EAAC,CAAC;AACvI;AASA,SAAS,iBAAiB,QAAQ,qBAAqB;AACnD,QAAM,UAAU,CAAC,QAAQ,sBAAsB,IAAI,YAAY,QAAQ,mBAAmB,IAAI,IAAI,EAAE,OAAO,OAAO;AAClH,MAAI,QAAQ,SAAS,GAAG;AACpB,WAAO,QAAQ,CAAC;AAAA,EACpB;AACA,SAAO,YAAY,IAAI,OAAO;AAClC;AAUA,eAAe,qBAAqB,UAAU,oBAAoB;AAC9D,MAAI,CAAC,sBAAsB,CAAC,SAAS,MAAM;AACvC,WAAO,SAAS,KAAI;AAAA,EACxB;AACA,QAAM,QAAQ,OAAO,SAAS,QAAQ,IAAI,gBAAgB,CAAC,KAAK;AAChE,QAAM,SAAS,SAAS,KAAK,UAAS;AACtC,QAAM,SAAS,CAAA;AACf,MAAI,SAAS;AACb,WAAS,OAAO,MAAM,OAAO,KAAI,GAAI,CAAC,KAAK,MAAM,OAAO,MAAM,OAAO,KAAI,GAAI;AACzE,WAAO,KAAK,KAAK,KAAK;AACtB,cAAU,KAAK,MAAM;AACrB,uBAAmB,EAAC,QAAQ,MAAK,CAAC;AAAA,EACtC;AACA,SAAO,IAAI,KAAK,QAAQ,EAAC,MAAM,SAAS,QAAQ,IAAI,cAAc,KAAK,GAAE,CAAC;AAC9E;AAkEO,SAAS,gBAAgB;AAAA,EAC5B,UAAU;AAAA,EACV,cAAc;AAAA,EACd;AAAA,EACA,SAAS,gBAAgB,CAAA;AAAA,EACzB,uBAAuB;AAAA,EACvB,OAAO;AAAA,EACP,qBAAqB,6BAA6B;AAAA,EAClD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY,CAAC,SAAS;AAAA,EACtB,cAAc,CAAC,SAAS;AAAA,EACxB,OAAO;AACX,IAAI,IAAI;AAEJ,QAAM,SAAS,CAAC,WAAW,YAAY,mBAAmB;AACtD,QAAI;AACA,gBAAU,GAAG,cAAc;AAAA,IAC/B,SAAS,OAAO;AACZ,UAAI,MAAM,aAAa,SAAS,uBAAuB,EAAC,SAAS,OAAO,QAAO,CAAC;AAAA,IACpF;AAAA,EACJ;AAEA,QAAM,UAAU,MAAM,uBAAuB,WAAW;AACxD,QAAM,iBAAiB,MAAM,cAAc;AAC3C,QAAM,iBAAiB,MAAM,cAAc;AAG3C,QAAM,YAAY,CAAC,KAAK,MAAM,iBAAiB;AAC3C,UAAM,UAAU;AAAA,MACZ,QAAQ;AAAA,MACR,mBAAmB,kBAAkB,YAAY;AAAA,MACjD,GAAG;AAAA,MACH,GAAG;AAAA,IACf;AACQ,QAAI,SAAS,UAAa,CAAC,UAAU,IAAI,GAAG;AACxC,cAAQ,cAAc,IAAI;AAAA,IAC9B;AACA,UAAM,QAAQ,WAAQ;AACtB,QAAI,OAAO;AACP,cAAQ,gBAAgB,UAAU,KAAK;AAAA,IAC3C;AACA,UAAM,YAAY,QAAQ,aAAa,GAAG,IAAI,WAAW,cAAc,IAAI;AAC3E,QAAI,WAAW;AACX,cAAQ,cAAc,IAAI;AAAA,IAC9B;AACA,WAAO;AAAA,EACX;AAGA,QAAM,OAAO,OAAO,QAAQ,KAAK,EAAC,MAAM,QAAQ,SAAS,oBAAmB,MAAM;AAC9E,UAAM,eAAe,UAAU,KAAK,MAAM,OAAO;AACjD,QAAI;AACA,aAAO,MAAM,QAAO,EAAG,KAAK;AAAA,QACxB;AAAA,QACA,SAAS;AAAA,QACT,MAAM,SAAS,UAAa,UAAU,IAAI,IAAI,OAAO,KAAK,UAAU,IAAI;AAAA,QACxE;AAAA,QACA,QAAQ,iBAAiB,QAAQ,mBAAmB;AAAA,MACpE,CAAa;AAAA,IACL,SAAS,OAAO;AAEZ,UAAI,QAAQ,SAAS;AACjB,cAAM;AAAA,MACV;AACA,YAAM,SAAS,OAAO,SAAS,iBAAiB,uBAAuB;AACvE,UAAI,MAAM,QAAQ,EAAC,QAAQ,KAAK,OAAO,OAAO,KAAI,CAAC;AACnD,YAAM,IAAI,SAAS,EAAC,QAAQ,iBAAiB,QAAO,CAAC;AAAA,IACzD;AAAA,EACJ;AAiBA,QAAM,UAAU,OAAO,QAAQ,MAAM,UAAU,CAAA,MAAO;AAClD,UAAM,MAAM,SAAS,SAAS,MAAM,QAAQ,MAAM;AAClD,QAAI;AACA,aAAO,MAAM,YAAY,QAAQ,KAAK,OAAO;AAAA,IACjD,SAAS,OAAO;AAEZ,UAAI,iBAAiB,UAAU;AAC3B,eAAO,WAAW,SAAS,OAAO,EAAC,QAAQ,IAAG,CAAC;AAAA,MACnD;AACA,YAAM;AAAA,IACV;AAAA,EACJ;AAEA,QAAM,cAAc,OAAO,QAAQ,KAAK;AAAA,IACpC;AAAA,IAAM;AAAA,IAAQ;AAAA,IAAS,QAAQ,mBAAmB;AAAA,IAAM,6BAA6B;AAAA,IACrF,sBAAsB;AAAA,EAC9B,IAAQ,OAAO;AACP,WAAO,aAAa,WAAW,EAAC,QAAQ,IAAG,CAAC;AAC5C,UAAM,YAAY,KAAK,IAAG;AAC1B,QAAI,WAAW,MAAM,KAAK,QAAQ,KAAK,EAAC,MAAM,QAAQ,SAAS,oBAAmB,CAAC;AAGnF,QAAI,SAAS,WAAW,8BAA8B,MAAM,aAAa;AACrE,UAAI,KAAK,iDAAiD,EAAC,QAAQ,IAAG,CAAC;AACvE,YAAM,KAAK,OAAO,SAAS,SAAS,KAAK,WAAW,GAAG,EAAC,QAAQ,oBAAmB,CAAC;AACpF,iBAAW,MAAM,KAAK,QAAQ,KAAK,EAAC,MAAM,QAAQ,SAAS,oBAAmB,CAAC;AAAA,IACnF;AAEA,WAAO,cAAc,YAAY,EAAC,QAAQ,KAAK,QAAQ,SAAS,QAAQ,sBAAsB,KAAK,IAAG,IAAK,UAAS,CAAC;AAGrH,QAAI,sBAAsB;AACtB,2BAAqB,SAAS,QAAQ,IAAI,oBAAoB,CAAC;AAAA,IACnE;AAEA,QAAI,CAAC,SAAS,IAAI;AACd,YAAM,UAAU,gBAAgB,WAAW,MAAM,YAAY,QAAQ,GAAG,QAAQ;AAEhF,YAAM,oBAAoB,UAAU,KAAK,QAAW,OAAO,EAAE,iBAAiB;AAC9E,YAAM,mBAAmB,SAAS,QAAQ,IAAI,kBAAkB;AAChE,YAAM,WAAW,IAAI,SAAS;AAAA,QAC1B,QAAQ,SAAS;AAAA,QACjB;AAAA,QACA,oBAAoB,CAAC,oBAAoB,iBAAiB,WAAW,iBAAiB;AAAA,QACtF,mBAAmB,oBAAoB,SAAS,QAAQ,IAAI,aAAa,CAAC;AAAA,MAC1F,CAAa;AAED,UAAI,SAAS,UAAU,sBAAsB,UAAU,OAAO,EAAE,GAAG,MAAM,IAAI,GAAG,MAAM,SAAS,MAAM,IAAI;AAAA,QACrG,MAAM,SAAS;AAAA,QAAM,WAAW,SAAS,aAAa,SAAS,QAAQ,IAAI,cAAc;AAAA,MACzG,CAAa;AACD,UAAI,SAAS,qBAAqB,CAAC,4BAA4B;AAC3D,4BAAoB,QAAQ;AAAA,MAChC;AACA,YAAM;AAAA,IACV;AAEA,QAAI,UAAU,mBAAmB,UAAU;AACvC,aAAO;AAAA,IACX;AACA,QAAI,UAAU,mBAAmB,MAAM;AACnC,aAAO,SAAS,KAAI;AAAA,IACxB;AACA,QAAI,SAAS,WAAW,mBAAmB;AACvC,aAAO;AAAA,IACX;AAEA,UAAM,OAAO,MAAM,SAAS,KAAI;AAChC,QAAI,CAAC,MAAM;AACP,aAAO;AAAA,IACX;AACA,QAAI;AACJ,QAAI;AACA,aAAO,KAAK,MAAM,IAAI;AAAA,IAC1B,QAAQ;AAEJ,UAAI,MAAM,GAAG,MAAM,IAAI,GAAG,kCAAkC,EAAC,aAAa,SAAS,QAAQ,IAAI,cAAc,EAAC,CAAC;AAC/G,YAAM,IAAI,SAAS,EAAC,QAAQ,oBAAmB,CAAC;AAAA,IACpD;AACA,WAAO,YAAY,MAAM,EAAC,QAAQ,IAAG,CAAC;AAAA,EAC1C;AAeA,QAAM,WAAW,OAAO,MAAM,EAAC,QAAQ,QAAQ,oBAAoB,mBAAmB,YAAY,sBAAsB,WAAU,IAAI,CAAA,MAAO;AACzI,UAAM,WAAW,MAAM,QAAQ,OAAO,MAAM,EAAC,QAAQ,QAAQ,OAAO,mBAAmB,UAAU,oBAAmB,CAAC;AACrH,UAAM,OAAO,MAAM,qBAAqB,UAAU,kBAAkB;AACpE,UAAM,WAAW,+BAA+B,SAAS,QAAQ,IAAI,qBAAqB,GAAG,gBAAgB;AAE7G,UAAM,cAAc,OAAO,SAAS,QAAQ,IAAI,cAAc,KAAK,EAAE,EAAE,MAAM,GAAG,EAAE,CAAC,EAAE,KAAI;AACzF,QAAI,gBAAgB,qBAAqB,CAAE,MAAM,cAAc,IAAI,GAAI;AACnE,YAAM,YAAY,SAAS,QAAQ,IAAI,cAAc;AACrD,UAAI,MAAM,OAAO,IAAI,qEAAqE,EAAC,WAAW,SAAQ,CAAC;AAC/G,YAAM,IAAI,SAAS,EAAC,QAAQ,qBAAqB,SAAS,EAAC,YAAY,UAAS,EAAC,CAAC;AAAA,IACtF;AACA,WAAO,EAAC,MAAM,SAAQ;AAAA,EAC1B;AAEA,SAAO;AAAA,IACH;AAAA,IACA,KAAK,CAAC,MAAM,YAAY,QAAQ,OAAO,MAAM,OAAO;AAAA,IACpD,MAAM,CAAC,MAAM,MAAM,YAAY,QAAQ,QAAQ,MAAM,EAAC,GAAG,SAAS,KAAI,CAAC;AAAA,IACvE,KAAK,CAAC,MAAM,MAAM,YAAY,QAAQ,OAAO,MAAM,EAAC,GAAG,SAAS,KAAI,CAAC;AAAA,IACrE,OAAO,CAAC,MAAM,MAAM,YAAY,QAAQ,SAAS,MAAM,EAAC,GAAG,SAAS,KAAI,CAAC;AAAA,IACzE,QAAQ,CAAC,MAAM,YAAY,QAAQ,UAAU,MAAM,OAAO;AAAA,IAC1D;AAAA,EACR;AACA;"}
@@ -2,7 +2,6 @@
2
2
  import { jsxs, Fragment, jsx } from "react/jsx-runtime";
3
3
  import { useState } from "react";
4
4
  import { useToolkitTranslation } from "../toolkit/useToolkitTranslation.js";
5
- import { useToolkitNavigation } from "../toolkit/toolkitContext.js";
6
5
  import Avatar from "@mui/material/Avatar";
7
6
  import Box from "@mui/material/Box";
8
7
  import Divider from "@mui/material/Divider";
@@ -15,11 +14,13 @@ import Tooltip from "@mui/material/Tooltip";
15
14
  import Typography from "@mui/material/Typography";
16
15
  import LogoutIcon from "@mui/icons-material/Logout";
17
16
  import PersonOutlineIcon from "@mui/icons-material/PersonOutline";
18
- import { initialsOf, menuItemLabelOf } from "./menuItemText.js";
17
+ import { initialsOf } from "./menuItemText.js";
19
18
  import { barMenuPlacementOf } from "./barMenuPlacement.js";
19
+ import MenuLinks from "./MenuLinks.js";
20
+ import { WHITE_LABEL, MONOGRAM_GRAY } from "../theme/components/glassComponents.js";
21
+ import { isGlassTheme } from "../theme/glassSurface.js";
20
22
  function AccountMenu({ user, profilePath, accountMenuItems = [], onSignOut, isSigningOut = false, settings }) {
21
23
  const { t } = useToolkitTranslation("layout");
22
- const { Link: RouterLink } = useToolkitNavigation();
23
24
  const [anchorElement, setAnchorElement] = useState(null);
24
25
  const closeMenu = () => setAnchorElement(null);
25
26
  const menuLinks = [
@@ -32,7 +33,16 @@ function AccountMenu({ user, profilePath, accountMenuItems = [], onSignOut, isSi
32
33
  onSignOut();
33
34
  };
34
35
  return /* @__PURE__ */ jsxs(Fragment, { children: [
35
- /* @__PURE__ */ jsx(Tooltip, { title: t("account.open"), children: /* @__PURE__ */ jsx(IconButton, { size: "small", "aria-label": t("account.open"), "aria-haspopup": "menu", onClick: (event) => setAnchorElement(event.currentTarget), sx: { p: 0.5 }, children: /* @__PURE__ */ jsx(Avatar, { src: user.avatarUrl, alt: "", sx: { width: 32, height: 32, fontSize: "0.875rem", bgcolor: "secondary.main", color: "secondary.contrastText" }, children: initialsOf(user.name) }) }) }),
36
+ /* @__PURE__ */ jsx(Tooltip, { title: t("account.open"), children: /* @__PURE__ */ jsx(IconButton, { size: "small", "aria-label": t("account.open"), "aria-haspopup": "menu", onClick: (event) => setAnchorElement(event.currentTarget), sx: { p: 0.5 }, children: /* @__PURE__ */ jsx(Avatar, { src: user.avatarUrl, alt: "", sx: (theme) => ({
37
+ width: 32,
38
+ height: 32,
39
+ fontSize: "0.875rem",
40
+ bgcolor: "secondary.main",
41
+ color: "secondary.contrastText",
42
+ // con vidrio, en oscuro el monograma de Apple (gris oscuro con letra blanca), el que se aprobó. Antes lo
43
+ // ponía el tema porque su regla de oscuro le ganaba a este sx; ahora el sx gana en los dos modos
44
+ ...isGlassTheme(theme) && theme.applyStyles("dark", { bgcolor: MONOGRAM_GRAY.dark, color: WHITE_LABEL })
45
+ }), children: initialsOf(user.name) }) }) }),
36
46
  /* @__PURE__ */ jsxs(
37
47
  Menu,
38
48
  {
@@ -51,25 +61,7 @@ function AccountMenu({ user, profilePath, accountMenuItems = [], onSignOut, isSi
51
61
  settings && /* @__PURE__ */ jsx(Divider, {}),
52
62
  settings && /* @__PURE__ */ jsx(Box, { sx: { py: 0.5 }, children: settings }),
53
63
  menuLinks.length > 0 && /* @__PURE__ */ jsx(Divider, {}),
54
- menuLinks.map((menuLink) => {
55
- const Icon = menuLink.Icon;
56
- const linkProps = menuLink.to ? { component: RouterLink, to: menuLink.to } : {};
57
- return /* @__PURE__ */ jsxs(
58
- MenuItem,
59
- {
60
- ...linkProps,
61
- onClick: () => {
62
- closeMenu();
63
- menuLink.onClick?.();
64
- },
65
- children: [
66
- Icon && /* @__PURE__ */ jsx(ListItemIcon, { children: /* @__PURE__ */ jsx(Icon, { fontSize: "small" }) }),
67
- /* @__PURE__ */ jsx(ListItemText, { primary: menuItemLabelOf(menuLink, t) })
68
- ]
69
- },
70
- menuLink.to ?? menuItemLabelOf(menuLink, t)
71
- );
72
- }),
64
+ /* @__PURE__ */ jsx(MenuLinks, { links: menuLinks, onClose: closeMenu }),
73
65
  onSignOut && /* @__PURE__ */ jsx(Divider, {}),
74
66
  onSignOut && /* @__PURE__ */ jsxs(MenuItem, { onClick: signOut, disabled: isSigningOut, children: [
75
67
  /* @__PURE__ */ jsx(ListItemIcon, { children: /* @__PURE__ */ jsx(LogoutIcon, { fontSize: "small" }) }),
@@ -1 +1 @@
1
- {"version":3,"file":"AccountMenu.js","sources":["../../src/appShell/AccountMenu.jsx"],"sourcesContent":["// La cuenta de quien está dentro, arriba a la derecha (como en las plantillas de MUI): un avatar con sus iniciales (o su\n// foto) que abre un menú con su nombre y correo, Mi perfil, lo que el proyecto agregue (accountMenuItems: Facturación,\n// Mis llaves…) y Salir.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport Avatar from '@mui/material/Avatar';\nimport Box from '@mui/material/Box';\nimport Divider from '@mui/material/Divider';\nimport IconButton from '@mui/material/IconButton';\nimport ListItemIcon from '@mui/material/ListItemIcon';\nimport ListItemText from '@mui/material/ListItemText';\nimport Menu from '@mui/material/Menu';\nimport MenuItem from '@mui/material/MenuItem';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport LogoutIcon from '@mui/icons-material/Logout';\nimport PersonOutlineIcon from '@mui/icons-material/PersonOutline';\n\nimport {initialsOf, menuItemLabelOf} from '@/appShell/menuItemText.js';\nimport {barMenuPlacementOf} from '@/appShell/barMenuPlacement.js';\n\n/**\n * @param {object} props\n * @param {{name: string, email?: string, subtitle?: string, avatarUrl?: string}} props.user\n * @param {string} [props.profilePath] sin ella no sale «Mi perfil»\n * @param {Array<{to?: string, onClick?: Function, label?: string, labelKey?: string, Icon?: import('react').ElementType}>} [props.accountMenuItems]\n * @param {() => void} [props.onSignOut] sin él no sale «Salir»\n * @param {boolean} [props.isSigningOut]\n * @param {import('react').ReactNode} [props.settings] los ajustes de la app, entre los datos de la persona y sus ligas\n */\nexport default function AccountMenu({user, profilePath, accountMenuItems = [], onSignOut, isSigningOut = false, settings}) {\n const {t} = useToolkitTranslation('layout');\n const {Link: RouterLink} = useToolkitNavigation();\n const [anchorElement, setAnchorElement] = useState(null);\n const closeMenu = () => setAnchorElement(null);\n const menuLinks = [\n ...(profilePath ? [{to: profilePath, labelKey: 'layout:account.myProfile', Icon: PersonOutlineIcon}] : []),\n ...accountMenuItems,\n ];\n\n // hacia arriba si la barra está abajo (teléfono con vidrio)\n const placement = barMenuPlacementOf(anchorElement);\n\n const signOut = () => {\n closeMenu();\n onSignOut();\n };\n\n return (\n <>\n <Tooltip title={t('account.open')}>\n <IconButton size=\"small\" aria-label={t('account.open')} aria-haspopup=\"menu\" onClick={(event) => setAnchorElement(event.currentTarget)} sx={{p: 0.5}}>\n <Avatar src={user.avatarUrl} alt=\"\" sx={{width: 32, height: 32, fontSize: '0.875rem', bgcolor: 'secondary.main', color: 'secondary.contrastText'}}>\n {initialsOf(user.name)}\n </Avatar>\n </IconButton>\n </Tooltip>\n <Menu anchorEl={anchorElement} open={Boolean(anchorElement)} onClose={closeMenu}\n anchorOrigin={placement.anchorOrigin} transformOrigin={placement.transformOrigin}\n slotProps={{paper: {sx: {minWidth: settings ? 300 : 240, maxWidth: 360, ...placement.gapSx}}}}>\n <Box sx={{px: 2, py: 1.25}}>\n <Typography variant=\"subtitle2\" noWrap sx={{fontWeight: 600}}>{user.name}</Typography>\n {user.email && <Typography variant=\"body2\" color=\"text.secondary\" noWrap>{user.email}</Typography>}\n {user.subtitle && <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\" noWrap>{user.subtitle}</Typography>}\n </Box>\n {/* los ajustes de la app (letra, idioma, claro u oscuro y los del proyecto): en la cuenta y no sueltos en la barra */}\n {settings && <Divider />}\n {settings && <Box sx={{py: 0.5}}>{settings}</Box>}\n {menuLinks.length > 0 && <Divider />}\n {menuLinks.map((menuLink) => {\n const Icon = menuLink.Icon;\n const linkProps = menuLink.to ? {component: RouterLink, to: menuLink.to} : {};\n return (\n <MenuItem key={menuLink.to ?? menuItemLabelOf(menuLink, t)} {...linkProps}\n onClick={() => {\n closeMenu();\n menuLink.onClick?.();\n }}>\n {Icon && <ListItemIcon><Icon fontSize=\"small\" /></ListItemIcon>}\n <ListItemText primary={menuItemLabelOf(menuLink, t)} />\n </MenuItem>\n );\n })}\n {onSignOut && <Divider />}\n {onSignOut && (\n <MenuItem onClick={signOut} disabled={isSigningOut}>\n <ListItemIcon><LogoutIcon fontSize=\"small\" /></ListItemIcon>\n <ListItemText primary={t('signOut')} />\n </MenuItem>\n )}\n </Menu>\n </>\n );\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"AccountMenu.js","sources":["../../src/appShell/AccountMenu.jsx"],"sourcesContent":["// La cuenta de quien está dentro, arriba a la derecha (como en las plantillas de MUI): un avatar con sus iniciales (o su\n// foto) que abre un menú con su nombre y correo, Mi perfil, lo que el proyecto agregue (accountMenuItems: Facturación,\n// Mis llaves…) y Salir.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useState} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport Avatar from '@mui/material/Avatar';\nimport Box from '@mui/material/Box';\nimport Divider from '@mui/material/Divider';\nimport IconButton from '@mui/material/IconButton';\nimport ListItemIcon from '@mui/material/ListItemIcon';\nimport ListItemText from '@mui/material/ListItemText';\nimport Menu from '@mui/material/Menu';\nimport MenuItem from '@mui/material/MenuItem';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport LogoutIcon from '@mui/icons-material/Logout';\nimport PersonOutlineIcon from '@mui/icons-material/PersonOutline';\n\nimport {initialsOf} from '@/appShell/menuItemText.js';\nimport {barMenuPlacementOf} from '@/appShell/barMenuPlacement.js';\nimport MenuLinks from '@/appShell/MenuLinks.jsx';\nimport {MONOGRAM_GRAY, WHITE_LABEL} from '@/theme/components/glassComponents.js';\nimport {isGlassTheme} from '@/theme/glassSurface.js';\n\n/**\n * @param {object} props\n * @param {{name: string, email?: string, subtitle?: string, avatarUrl?: string}} props.user\n * @param {string} [props.profilePath] sin ella no sale «Mi perfil»\n * @param {Array<{to?: string, onClick?: Function, label?: string, labelKey?: string, Icon?: import('react').ElementType}>} [props.accountMenuItems]\n * @param {() => void} [props.onSignOut] sin él no sale «Salir»\n * @param {boolean} [props.isSigningOut]\n * @param {import('react').ReactNode} [props.settings] los ajustes de la app, entre los datos de la persona y sus ligas\n */\nexport default function AccountMenu({user, profilePath, accountMenuItems = [], onSignOut, isSigningOut = false, settings}) {\n const {t} = useToolkitTranslation('layout');\n const [anchorElement, setAnchorElement] = useState(null);\n const closeMenu = () => setAnchorElement(null);\n const menuLinks = [\n ...(profilePath ? [{to: profilePath, labelKey: 'layout:account.myProfile', Icon: PersonOutlineIcon}] : []),\n ...accountMenuItems,\n ];\n\n // hacia arriba si la barra está abajo (teléfono con vidrio)\n const placement = barMenuPlacementOf(anchorElement);\n\n const signOut = () => {\n closeMenu();\n onSignOut();\n };\n\n return (\n <>\n <Tooltip title={t('account.open')}>\n <IconButton size=\"small\" aria-label={t('account.open')} aria-haspopup=\"menu\" onClick={(event) => setAnchorElement(event.currentTarget)} sx={{p: 0.5}}>\n <Avatar src={user.avatarUrl} alt=\"\" sx={(theme) => ({\n width: 32, height: 32, fontSize: '0.875rem', bgcolor: 'secondary.main', color: 'secondary.contrastText',\n // con vidrio, en oscuro el monograma de Apple (gris oscuro con letra blanca), el que se aprobó. Antes lo\n // ponía el tema porque su regla de oscuro le ganaba a este sx; ahora el sx gana en los dos modos\n ...(isGlassTheme(theme) && theme.applyStyles('dark', {bgcolor: MONOGRAM_GRAY.dark, color: WHITE_LABEL})),\n })}>\n {initialsOf(user.name)}\n </Avatar>\n </IconButton>\n </Tooltip>\n <Menu anchorEl={anchorElement} open={Boolean(anchorElement)} onClose={closeMenu}\n anchorOrigin={placement.anchorOrigin} transformOrigin={placement.transformOrigin}\n slotProps={{paper: {sx: {minWidth: settings ? 300 : 240, maxWidth: 360, ...placement.gapSx}}}}>\n <Box sx={{px: 2, py: 1.25}}>\n <Typography variant=\"subtitle2\" noWrap sx={{fontWeight: 600}}>{user.name}</Typography>\n {user.email && <Typography variant=\"body2\" color=\"text.secondary\" noWrap>{user.email}</Typography>}\n {user.subtitle && <Typography variant=\"caption\" color=\"text.secondary\" component=\"p\" noWrap>{user.subtitle}</Typography>}\n </Box>\n {/* los ajustes de la app (letra, idioma, claro u oscuro y los del proyecto): en la cuenta y no sueltos en la barra */}\n {settings && <Divider />}\n {settings && <Box sx={{py: 0.5}}>{settings}</Box>}\n {menuLinks.length > 0 && <Divider />}\n <MenuLinks links={menuLinks} onClose={closeMenu} />\n {onSignOut && <Divider />}\n {onSignOut && (\n <MenuItem onClick={signOut} disabled={isSigningOut}>\n <ListItemIcon><LogoutIcon fontSize=\"small\" /></ListItemIcon>\n <ListItemText primary={t('signOut')} />\n </MenuItem>\n )}\n </Menu>\n </>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;;;;;;;;;AAmCA,SAAwB,YAAY,EAAC,MAAM,aAAa,mBAAmB,IAAI,WAAW,eAAe,OAAO,YAAW;AACvH,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ;AAC1C,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,IAAI;AACvD,QAAM,YAAY,MAAM,iBAAiB,IAAI;AAC7C,QAAM,YAAY;AAAA,IACd,GAAI,cAAc,CAAC,EAAC,IAAI,aAAa,UAAU,4BAA4B,MAAM,kBAAA,CAAkB,IAAI,CAAA;AAAA,IACvG,GAAG;AAAA,EAAA;AAIP,QAAM,YAAY,mBAAmB,aAAa;AAElD,QAAM,UAAU,MAAM;AAClB,cAAA;AACA,cAAA;AAAA,EACJ;AAEA,SACI,qBAAA,UAAA,EACI,UAAA;AAAA,IAAA,oBAAC,WAAQ,OAAO,EAAE,cAAc,GAC5B,8BAAC,YAAA,EAAW,MAAK,SAAQ,cAAY,EAAE,cAAc,GAAG,iBAAc,QAAO,SAAS,CAAC,UAAU,iBAAiB,MAAM,aAAa,GAAG,IAAI,EAAC,GAAG,OAC5I,UAAA,oBAAC,QAAA,EAAO,KAAK,KAAK,WAAW,KAAI,IAAG,IAAI,CAAC,WAAW;AAAA,MAChD,OAAO;AAAA,MAAI,QAAQ;AAAA,MAAI,UAAU;AAAA,MAAY,SAAS;AAAA,MAAkB,OAAO;AAAA;AAAA;AAAA,MAG/E,GAAI,aAAa,KAAK,KAAK,MAAM,YAAY,QAAQ,EAAC,SAAS,cAAc,MAAM,OAAO,aAAY;AAAA,IAAA,IAErG,UAAA,WAAW,KAAK,IAAI,EAAA,CACzB,GACJ,GACJ;AAAA,IACA;AAAA,MAAC;AAAA,MAAA;AAAA,QAAK,UAAU;AAAA,QAAe,MAAM,QAAQ,aAAa;AAAA,QAAG,SAAS;AAAA,QAChE,cAAc,UAAU;AAAA,QAAc,iBAAiB,UAAU;AAAA,QACjE,WAAW,EAAC,OAAO,EAAC,IAAI,EAAC,UAAU,WAAW,MAAM,KAAK,UAAU,KAAK,GAAG,UAAU,MAAA,IAAM;AAAA,QAC7F,UAAA;AAAA,UAAA,qBAAC,OAAI,IAAI,EAAC,IAAI,GAAG,IAAI,QACjB,UAAA;AAAA,YAAA,oBAAC,YAAA,EAAW,SAAQ,aAAY,QAAM,MAAC,IAAI,EAAC,YAAY,IAAA,GAAO,UAAA,KAAK,KAAA,CAAK;AAAA,YACxE,KAAK,SAAS,oBAAC,YAAA,EAAW,SAAQ,SAAQ,OAAM,kBAAiB,QAAM,MAAE,UAAA,KAAK,MAAA,CAAM;AAAA,YACpF,KAAK,YAAY,oBAAC,YAAA,EAAW,SAAQ,WAAU,OAAM,kBAAiB,WAAU,KAAI,QAAM,MAAE,eAAK,SAAA,CAAS;AAAA,UAAA,GAC/G;AAAA,UAEC,gCAAa,SAAA,EAAQ;AAAA,UACrB,gCAAa,KAAA,EAAI,IAAI,EAAC,IAAI,IAAA,GAAO,UAAA,UAAS;AAAA,UAC1C,UAAU,SAAS,KAAK,oBAAC,SAAA,CAAA,CAAQ;AAAA,UAClC,oBAAC,WAAA,EAAU,OAAO,WAAW,SAAS,WAAW;AAAA,UAChD,iCAAc,SAAA,EAAQ;AAAA,UACtB,aACG,qBAAC,UAAA,EAAS,SAAS,SAAS,UAAU,cAClC,UAAA;AAAA,YAAA,oBAAC,cAAA,EAAa,UAAA,oBAAC,YAAA,EAAW,UAAS,SAAQ,GAAE;AAAA,YAC7C,oBAAC,cAAA,EAAa,SAAS,EAAE,SAAS,EAAA,CAAG;AAAA,UAAA,EAAA,CACzC;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,EAER,GACJ;AAER;"}
@@ -22,6 +22,7 @@ import BarCapsule from "./BarCapsule.js";
22
22
  import { BarActionsSlotContext } from "./barActionsSlot.js";
23
23
  import HeaderTools from "./HeaderTools.js";
24
24
  import MobileTabBar from "./MobileTabBar.js";
25
+ import Wordmark from "./Wordmark.js";
25
26
  import { PageTitleProvider, usePageTitle } from "./pageTitle.js";
26
27
  import { GLASS_PANEL_GAP_PIXELS, DESKTOP_MEDIA_QUERY, APP_BAR_HEIGHT_PIXELS, PHONE_MEDIA_QUERY, DRAWER_WIDTH_COLLAPSED, DRAWER_WIDTH_EXPANDED, drawerTransitionOf, FLOATING_BOTTOM_SPACE_CSS_VARIABLE, MAIN_CONTENT_ID, MOBILE_TAB_BAR_HEIGHT_PIXELS } from "./shellSizes.js";
27
28
  import SideMenu from "./SideMenu.js";
@@ -30,7 +31,7 @@ import TopMenu from "./TopMenu.js";
30
31
  import { useDrawerCollapsed } from "./useDrawerCollapsed.js";
31
32
  import ComponentTexts from "../toolkit/ComponentTexts.js";
32
33
  import NewBuildNotice, { NEW_BUILD_NOTICE_VARIANT } from "../feedback/NewBuildNotice.js";
33
- import { activeMenuItemOf } from "../navigation/appMenu.js";
34
+ import { menuItemsWithIconsOf, activeMenuItemOf } from "../navigation/appMenu.js";
34
35
  import { useNavigationTrail } from "../navigationTrail.js";
35
36
  import { THEME_LOOK, TOP_BAR } from "../theme/createToolkitTheme.js";
36
37
  import { isGlassTheme, ambientBackgroundOf, scrollEdgeStylesOf, GLASS_PANEL_RADIUS_PIXELS } from "../theme/glassSurface.js";
@@ -59,21 +60,22 @@ const SkipToContentLink = () => {
59
60
  "&:focus": { top: 8 }
60
61
  }, children: t("skipToContent") });
61
62
  };
62
- const BarBrand = ({ logo, brandName, brandCaption, homePath, sx }) => {
63
+ const BarBrand = ({ logo, wordmark, brandName, brandCaption, homePath, sx }) => {
63
64
  const { Link: RouterLink } = useToolkitNavigation();
64
- return /* @__PURE__ */ jsxs(
65
+ return /* @__PURE__ */ jsx(
65
66
  ButtonBase,
66
67
  {
67
68
  component: RouterLink,
68
69
  to: homePath,
70
+ "aria-label": wordmark ? brandName : void 0,
69
71
  sx: [{ alignItems: "center", gap: 1.25, borderRadius: 1, flexShrink: 0, minWidth: 0, pr: 1, textAlign: "left" }, sx],
70
- children: [
72
+ children: wordmark ? /* @__PURE__ */ jsx(Wordmark, { children: wordmark }) : /* @__PURE__ */ jsxs(Fragment, { children: [
71
73
  logo && /* @__PURE__ */ jsx(Box, { sx: { width: 28, height: 28, flex: "0 0 auto", display: "flex", alignItems: "center", justifyContent: "center" }, children: logo }),
72
74
  /* @__PURE__ */ jsxs(Box, { sx: { minWidth: 0 }, children: [
73
75
  /* @__PURE__ */ jsx(Typography, { variant: "subtitle2", component: "span", noWrap: true, sx: { display: "block", fontWeight: 600, lineHeight: 1.3 }, children: brandName }),
74
76
  brandCaption && /* @__PURE__ */ jsx(Typography, { variant: "caption", component: "span", noWrap: true, sx: { display: "block", lineHeight: 1.3, opacity: 0.8 }, children: brandCaption })
75
77
  ] })
76
- ]
78
+ ] })
77
79
  }
78
80
  );
79
81
  };
@@ -113,6 +115,7 @@ const CollapseMenuButton = ({ collapsed, onToggle }) => {
113
115
  };
114
116
  const AppLayoutFrame = ({
115
117
  logo,
118
+ wordmark,
116
119
  brandName,
117
120
  brandCaption,
118
121
  homePath,
@@ -165,7 +168,7 @@ const AppLayoutFrame = ({
165
168
  const isBarMerged = isGlass && isSideMenu && collapsed && !ribbons && !usesBarCapsules;
166
169
  const drawerWidth = collapsed ? DRAWER_WIDTH_COLLAPSED : DRAWER_WIDTH_EXPANDED;
167
170
  const activeItem = activeMenuItemOf(menuItems, pathname);
168
- const sideMenuProps = { logo, brandName, brandCaption, homePath, menuItems, activeItem, menuBadges, footer: sidebarFooter };
171
+ const sideMenuProps = { logo, wordmark, brandName, brandCaption, homePath, menuItems, activeItem, menuBadges, footer: sidebarFooter };
169
172
  const [barActionsSlot, setBarActionsSlot] = useState(null);
170
173
  const usesPhoneFloatingActions = usesTabBar && usesBarCapsules;
171
174
  const [phoneBarActionsSlot, setPhoneBarActionsSlot] = useState(null);
@@ -249,6 +252,7 @@ const AppLayoutFrame = ({
249
252
  BarBrand,
250
253
  {
251
254
  logo,
255
+ wordmark,
252
256
  brandName,
253
257
  brandCaption,
254
258
  homePath,
@@ -443,6 +447,7 @@ const AppLayoutFrame = ({
443
447
  };
444
448
  const AppLayout = ({
445
449
  logo,
450
+ wordmark,
446
451
  brandName,
447
452
  brandCaption,
448
453
  homePath = "/",
@@ -472,15 +477,17 @@ const AppLayout = ({
472
477
  children
473
478
  }) => {
474
479
  const { appName } = useToolkitSettings();
475
- const appMenu = useMemo(() => ({ menuItems }), [menuItems]);
480
+ const shownMenuItems = useMemo(() => menuItemsWithIconsOf(menuItems), [menuItems]);
481
+ const appMenu = useMemo(() => ({ menuItems: shownMenuItems }), [shownMenuItems]);
476
482
  return /* @__PURE__ */ jsx(ComponentTexts, { namespace: "layout", texts, children: /* @__PURE__ */ jsx(PageTitleProvider, { children: /* @__PURE__ */ jsx(AppMenuContext.Provider, { value: appMenu, children: /* @__PURE__ */ jsx(
477
483
  AppLayoutFrame,
478
484
  {
479
485
  logo,
486
+ wordmark,
480
487
  brandName: brandName ?? appName,
481
488
  brandCaption,
482
489
  homePath,
483
- menuItems,
490
+ menuItems: shownMenuItems,
484
491
  menuPosition,
485
492
  user,
486
493
  profilePath,