@stackcraftmx/calcita 0.25.0 → 0.27.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 (60) hide show
  1. package/README.md +29 -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 +8 -4
  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 +28 -9
  38. package/dist/theme/components/minimalComponents.js.map +1 -1
  39. package/dist/theme/createToolkitTheme.js +6 -3
  40. package/dist/theme/createToolkitTheme.js.map +1 -1
  41. package/dist/theme/glassSurface.js +36 -38
  42. package/dist/theme/glassSurface.js.map +1 -1
  43. package/dist/theme/lightDarkStyles.js +48 -0
  44. package/dist/theme/lightDarkStyles.js.map +1 -0
  45. package/dist/types/api/createApiClient.d.ts +17 -0
  46. package/dist/types/appShell/AppLayout.d.ts +5 -1
  47. package/dist/types/appShell/MenuLinks.d.ts +17 -0
  48. package/dist/types/appShell/SettingsMenu.d.ts +3 -1
  49. package/dist/types/appShell/SideMenu.d.ts +3 -1
  50. package/dist/types/appShell/Wordmark.d.ts +6 -0
  51. package/dist/types/appShell/menuItemText.d.ts +16 -0
  52. package/dist/types/index.d.ts +4 -4
  53. package/dist/types/navigation/appMenu.d.ts +8 -0
  54. package/dist/types/observability/logger.d.ts +13 -0
  55. package/dist/types/theme/components/MuiAppBar.d.ts +3 -1
  56. package/dist/types/theme/components/glassComponents.d.ts +5 -0
  57. package/dist/types/theme/components/minimalComponents.d.ts +49 -3
  58. package/dist/types/theme/createToolkitTheme.d.ts +21 -1
  59. package/dist/types/theme/lightDarkStyles.d.ts +8 -0
  60. package/package.json +3 -2
@@ -1 +1 @@
1
- {"version":3,"file":"appMenu.js","sources":["../../src/navigation/appMenu.js"],"sourcesContent":["// El menú de la app: COMPUESTO, no escrito.\n//\n// Cada módulo declara su entrada en su propia carpeta (modules/<módulo>/menu.js) y aquí solo se juntan, se filtran por\n// permiso y se ordenan. Módulo nuevo con menú = cero ediciones en una lista central, y nadie pisa la entrada del vecino al\n// resolver un merge.\n//\n// Forma de un menu.js:\n// export const order = 10; // posición en el menú (los que no dicen, al final)\n// export const access = {ability: 'orders.view'}; // la regla de quién lo ve (accessRules.js); sin ella, todos\n// export function buildMenuItem(context) { // o buildMenuItems (en plural) para dos o más entradas\n// return {\n// to: '/orders', label: 'Pedidos', // o labelKey: 'orders:menu.label' (se traduce al pintarse)\n// description: 'Todos los pedidos…', // o descriptionKey: para las tarjetas de un inicio\n// section: 'Operación', // o sectionKey: el título de la sección del menú lateral\n// Icon: ReceiptLongOutlinedIcon,\n// badge: 3, // opcional: el numerito o la palabra a la derecha («Nuevo»)\n// mobileTab: true, // opcional: va en la barra de pestañas del teléfono (mobileTabsOf)\n// badgeKey: 'pendingPayment', // opcional: su conteo llega del servidor (AppLayout menuBadges)\n// matchPrefix: '/orders', // opcional: desde dónde se marca activo (por omisión, to)\n// access: {...}, // opcional: una entrada con otra regla que su módulo\n// children: [{to: '/orders/returns', label: 'Devoluciones', access: {...}}], // opcional: un submenú\n// };\n// }\n// buildMenuItem puede regresar null (una pantalla apagada por configuración): no sale. Un submenú se filtra igual que las\n// entradas, y si se queda sin hijas (y no lleva a ninguna pantalla propia) no sale.\n//\n// El proyecto le pasa los módulos con el glob de Vite (o una lista, en otro empaquetador):\n// const menuModules = import.meta.glob('@/modules/*/menu.js', {eager: true});\n// const menuItems = composeMenu(menuModules, {canAccess, context: {session}});\n//\n// Funciones puras, sin React: function (regla de la casa). Se prueban con node --test (appMenu.test.js).\n\n// la posición de un módulo que no dice su order: al final\nconst LAST_ORDER = 1000;\n\n// la dirección del inicio: la primera miga\nconst HOME_PATH = '/';\n\n/**\n * Las entradas del menú que le tocan a esta sesión, ordenadas.\n *\n * @param {Record<string, any>|any[]} menuModules lo que regresa import.meta.glob(..., {eager: true}) o una lista\n * @param {object} [options]\n * @param {(access: object|undefined) => boolean} [options.canAccess] la regla de permisos del proyecto; sin ella, todos\n * @param {object} [options.context] lo que reciben buildMenuItem(s): la sesión, la configuración…\n * @returns {object[]} las entradas, cada una con to y su texto\n */\nexport function composeMenu(menuModules, {canAccess = () => true, context = {}} = {}) {\n const modules = Array.isArray(menuModules) ? menuModules : Object.values(menuModules);\n return modules\n .filter((menuModule) => canAccess(menuModule.access))\n .sort((first, second) => (first.order ?? LAST_ORDER) - (second.order ?? LAST_ORDER))\n .flatMap((menuModule) => menuModule.buildMenuItems?.(context) ?? [menuModule.buildMenuItem?.(context)])\n .filter((menuItem) => Boolean(menuItem) && canAccess(menuItem.access))\n .map((menuItem) => withAllowedChildren(menuItem, canAccess))\n .filter((menuItem) => Boolean(menuItem.to) || menuItem.children?.length > 0);\n}\n\n/**\n * La entrada con solo las hijas que la sesión puede ver (una entrada sin submenú se queda igual).\n *\n * @param {object} menuItem\n * @param {(access: object|undefined) => boolean} canAccess\n * @returns {object}\n */\nfunction withAllowedChildren(menuItem, canAccess) {\n if (!Array.isArray(menuItem.children)) {\n return menuItem;\n }\n return {...menuItem, children: menuItem.children.filter((child) => Boolean(child) && canAccess(child.access))};\n}\n\n/**\n * Las entradas que llevan a una pantalla, con las de los submenús (las de arriba primero).\n *\n * @param {object[]} menuItems lo que regresa composeMenu\n * @returns {object[]}\n */\nexport function screenMenuItemsOf(menuItems) {\n return menuItems.flatMap((menuItem) => [menuItem, ...(menuItem.children ?? [])]).filter((menuItem) => Boolean(menuItem.to));\n}\n\n/**\n * ¿Está la entrada dentro de un submenú?\n *\n * @param {object[]} menuItems\n * @param {object} menuItem\n * @returns {boolean}\n */\nfunction isChildMenuItem(menuItems, menuItem) {\n return menuItems.some((candidate) => candidate.children?.includes(menuItem));\n}\n\n/**\n * La entrada del menú de la pantalla abierta: la más específica gana (/orders/15 marca «Pedidos», no «Inicio»).\n *\n * @param {Array<{to: string, matchPrefix?: string}>} menuItems lo que regresa composeMenu\n * @param {string} pathname location.pathname\n * @returns {object|null}\n */\nexport function activeMenuItemOf(menuItems, pathname) {\n const matchingItems = screenMenuItemsOf(menuItems).filter((menuItem) => {\n const prefix = menuItem.matchPrefix ?? menuItem.to;\n // el inicio solo se marca en el inicio: todas las direcciones empiezan con '/'\n const isHome = prefix === HOME_PATH;\n return pathname === prefix || (!isHome && pathname.startsWith(`${prefix}/`));\n });\n matchingItems.sort((first, second) => {\n const lengthDifference = (second.matchPrefix ?? second.to).length - (first.matchPrefix ?? first.to).length;\n if (lengthDifference !== 0) {\n return lengthDifference;\n }\n // con el mismo camino (un submenú cuya primera hija lleva a donde mismo que él), gana la hija: es la pantalla\n return Number(isChildMenuItem(menuItems, second)) - Number(isChildMenuItem(menuItems, first));\n });\n return matchingItems[0] ?? null;\n}\n\n/**\n * El submenú que contiene a una entrada (para abrirlo y para las migas); null si la entrada está arriba.\n *\n * @param {object[]} menuItems lo que regresa composeMenu\n * @param {object|null} menuItem\n * @returns {object|null}\n */\nexport function parentMenuItemOf(menuItems, menuItem) {\n return menuItems.find((candidate) => candidate.children?.includes(menuItem)) ?? null;\n}\n\n/**\n * Las migas de la pantalla abierta, sacadas del menú: el inicio, el submenú (si lo hay) y la entrada activa.\n * En el inicio, o en una pantalla que no está en el menú, regresa solo lo que encuentra (una o ninguna).\n *\n * @param {object[]} menuItems lo que regresa composeMenu\n * @param {string} pathname location.pathname\n * @returns {object[]} entradas del menú, en orden\n */\nexport function menuBreadcrumbsOf(menuItems, pathname) {\n const activeItem = activeMenuItemOf(menuItems, pathname);\n if (!activeItem) {\n return [];\n }\n const homeItem = menuItems.find((menuItem) => menuItem.to === HOME_PATH) ?? null;\n const parentItem = parentMenuItemOf(menuItems, activeItem);\n return [homeItem, parentItem, activeItem].filter((menuItem, index, crumbs) => Boolean(menuItem) && crumbs.indexOf(menuItem) === index);\n}\n\n/**\n * El badge de una entrada del menú: el conteo que llegó del servidor por su badgeKey (lo pendiente de esa pantalla), la\n * suma de sus hijas si es un submenú (como las carpetas del iPhone), o el badge fijo de su menu.js («Nuevo»). Un conteo\n * en cero no se enseña: no hay nada pendiente.\n *\n * menuBadgeOf({badgeKey: 'pendingPayment'}, {pendingPayment: 13}) // {content: 13, isCount: true}\n *\n * @param {object} menuItem una entrada de composeMenu\n * @param {Record<string, number>} [menuBadges] los conteos por badgeKey (el proyecto los pide a su servidor)\n * @returns {{content: number|string, isCount: boolean}|null}\n */\nexport function menuBadgeOf(menuItem, menuBadges = {}) {\n const countOf = (item) => {\n const count = item.badgeKey ? Number(menuBadges[item.badgeKey] ?? 0) : 0;\n return Number.isFinite(count) && count > 0 ? count : 0;\n };\n const ownCount = countOf(menuItem);\n const childrenCount = Array.isArray(menuItem.children) ? menuItem.children.reduce((total, child) => total + countOf(child), 0) : 0;\n const total = ownCount + childrenCount;\n if (total > 0) {\n return {content: total, isCount: true};\n }\n if (menuItem.badge !== undefined && menuItem.badge !== null && menuItem.badge !== '') {\n return {content: menuItem.badge, isCount: typeof menuItem.badge === 'number'};\n }\n return null;\n}\n\n// cuántas pestañas van en la barra del teléfono si no se dice otra cosa, como las de Apple: hasta 5; si hay más entradas,\n// 4 y «Más» con el resto. La barra (MobileTabBar) pasa las que de verdad caben a lo ancho\nconst MOBILE_TAB_LIMIT = 5;\n/** Cuántas puede fijar la persona en la barra: las que caben junto a «Más». */\nexport const PINNABLE_TAB_LIMIT = MOBILE_TAB_LIMIT - 1;\n\n/**\n * Con qué se reconoce una entrada del menú para recordarla (las pestañas que fijó la persona): su ruta, o su llave de\n * texto si no navega. No el texto traducido: cambia con el idioma.\n *\n * @param {{to?: string, labelKey?: string, label?: string}} menuItem\n * @returns {string}\n */\nexport function menuItemKeyOf(menuItem) {\n return menuItem.to ?? menuItem.labelKey ?? menuItem.label;\n}\n\n/**\n * Las pestañas de la barra del teléfono y lo que va en «Más». Van las que fijó la persona (pinnedKeys, en el orden del\n * menú); si no fijó ninguna, las que el menu.js marca con mobileTab: true; si ninguna lo dice, las primeras. Si todas\n * las entradas caben (tabLimit), van todas y no hay «Más»; si no, tabLimit - 1 y «Más» con el resto.\n *\n * @param {object[]} menuItems lo que regresa composeMenu\n * @param {string[]|null} [pinnedKeys] las que eligió la persona (menuItemKeyOf); las que ya no existen o ya no puede ver\n * no cuentan\n * @param {number} [tabLimit] cuántas caben en la barra contando «Más» (por omisión 5; la barra mide su ancho)\n * @returns {{tabs: object[], more: object[]}}\n */\nexport function mobileTabsOf(menuItems, pinnedKeys = null, tabLimit = MOBILE_TAB_LIMIT) {\n if (menuItems.length <= tabLimit) {\n return {tabs: menuItems, more: []};\n }\n const pinned = menuItems.filter((menuItem) => (pinnedKeys ?? []).includes(menuItemKeyOf(menuItem)));\n const marked = menuItems.filter((menuItem) => menuItem.mobileTab);\n let candidates = menuItems;\n if (pinned.length > 0) {\n candidates = pinned;\n } else if (marked.length > 0) {\n candidates = marked;\n }\n // una de las que caben es para «Más»\n const tabs = candidates.slice(0, Math.max(tabLimit - 1, 1));\n return {tabs, more: menuItems.filter((menuItem) => !tabs.includes(menuItem))};\n}\n"],"names":["total"],"mappings":"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"}
1
+ {"version":3,"file":"appMenu.js","sources":["../../src/navigation/appMenu.js"],"sourcesContent":["// El menú de la app: COMPUESTO, no escrito.\n//\n// Cada módulo declara su entrada en su propia carpeta (modules/<módulo>/menu.js) y aquí solo se juntan, se filtran por\n// permiso y se ordenan. Módulo nuevo con menú = cero ediciones en una lista central, y nadie pisa la entrada del vecino al\n// resolver un merge.\n//\n// Forma de un menu.js:\n// export const order = 10; // posición en el menú (los que no dicen, al final)\n// export const access = {ability: 'orders.view'}; // la regla de quién lo ve (accessRules.js); sin ella, todos\n// export function buildMenuItem(context) { // o buildMenuItems (en plural) para dos o más entradas\n// return {\n// to: '/orders', label: 'Pedidos', // o labelKey: 'orders:menu.label' (se traduce al pintarse)\n// description: 'Todos los pedidos…', // o descriptionKey: para las tarjetas de un inicio\n// section: 'Operación', // o sectionKey: el título de la sección del menú lateral\n// Icon: ReceiptLongOutlinedIcon, // o icon: <Iconify icon=\"mdi:receipt\" /> (un ícono ya pintado)\n// href: 'https://sistema-anterior/inicio', // en lugar de to: una liga a otro sistema (carga la página completa)\n// badge: 3, // opcional: el numerito o la palabra a la derecha («Nuevo»)\n// mobileTab: true, // opcional: va en la barra de pestañas del teléfono (mobileTabsOf)\n// badgeKey: 'pendingPayment', // opcional: su conteo llega del servidor (AppLayout menuBadges)\n// matchPrefix: '/orders', // opcional: desde dónde se marca activo (por omisión, to)\n// access: {...}, // opcional: una entrada con otra regla que su módulo\n// children: [{to: '/orders/returns', label: 'Devoluciones', access: {...}}], // opcional: un submenú\n// };\n// }\n// buildMenuItem puede regresar null (una pantalla apagada por configuración): no sale. Un submenú se filtra igual que las\n// entradas, y si se queda sin hijas (y no lleva a ninguna pantalla propia) no sale.\n//\n// El proyecto le pasa los módulos con el glob de Vite (o una lista, en otro empaquetador):\n// const menuModules = import.meta.glob('@/modules/*/menu.js', {eager: true});\n// const menuItems = composeMenu(menuModules, {canAccess, context: {session}});\n//\n// Funciones puras, sin React: function (regla de la casa). Se prueban con node --test (appMenu.test.js).\n\n// la posición de un módulo que no dice su order: al final\nconst LAST_ORDER = 1000;\n\n// la dirección del inicio: la primera miga\nconst HOME_PATH = '/';\n\n/**\n * Las entradas del menú que le tocan a esta sesión, ordenadas.\n *\n * @param {Record<string, any>|any[]} menuModules lo que regresa import.meta.glob(..., {eager: true}) o una lista\n * @param {object} [options]\n * @param {(access: object|undefined) => boolean} [options.canAccess] la regla de permisos del proyecto; sin ella, todos\n * @param {object} [options.context] lo que reciben buildMenuItem(s): la sesión, la configuración…\n * @returns {object[]} las entradas, cada una con to y su texto\n */\nexport function composeMenu(menuModules, {canAccess = () => true, context = {}} = {}) {\n const modules = Array.isArray(menuModules) ? menuModules : Object.values(menuModules);\n return modules\n .filter((menuModule) => canAccess(menuModule.access))\n .sort((first, second) => (first.order ?? LAST_ORDER) - (second.order ?? LAST_ORDER))\n .flatMap((menuModule) => menuModule.buildMenuItems?.(context) ?? [menuModule.buildMenuItem?.(context)])\n .filter((menuItem) => Boolean(menuItem) && canAccess(menuItem.access))\n .map((menuItem) => withAllowedChildren(menuItem, canAccess))\n .filter((menuItem) => Boolean(menuItem.to) || Boolean(menuItem.href) || menuItem.children?.length > 0);\n}\n\n/**\n * Las entradas con su ícono como componente (Icon), que es lo que pintan las piezas del menú: el proyecto puede dar el\n * componente o un ícono ya pintado (icon, por ejemplo <Iconify icon=\"mdi:bank\" /> de un sistema que ya usaba iconify).\n *\n * @param {object[]} menuItems\n * @returns {object[]}\n */\nexport function menuItemsWithIconsOf(menuItems) {\n return menuItems.map((menuItem) => {\n const Icon = menuItem.Icon ?? (menuItem.icon ? iconComponentOf(menuItem.icon) : undefined);\n const children = Array.isArray(menuItem.children) ? menuItemsWithIconsOf(menuItem.children) : menuItem.children;\n return {...menuItem, Icon, children};\n });\n}\n\n/** Un componente que pinta el ícono ya hecho (no recibe el tamaño: el proyecto lo pinta a su gusto). */\nfunction iconComponentOf(icon) {\n const MenuIcon = () => icon;\n return MenuIcon;\n}\n\n/**\n * La entrada con solo las hijas que la sesión puede ver (una entrada sin submenú se queda igual).\n *\n * @param {object} menuItem\n * @param {(access: object|undefined) => boolean} canAccess\n * @returns {object}\n */\nfunction withAllowedChildren(menuItem, canAccess) {\n if (!Array.isArray(menuItem.children)) {\n return menuItem;\n }\n return {...menuItem, children: menuItem.children.filter((child) => Boolean(child) && canAccess(child.access))};\n}\n\n/**\n * Las entradas que llevan a una pantalla, con las de los submenús (las de arriba primero).\n *\n * @param {object[]} menuItems lo que regresa composeMenu\n * @returns {object[]}\n */\nexport function screenMenuItemsOf(menuItems) {\n return menuItems.flatMap((menuItem) => [menuItem, ...(menuItem.children ?? [])]).filter((menuItem) => Boolean(menuItem.to));\n}\n\n/**\n * ¿Está la entrada dentro de un submenú?\n *\n * @param {object[]} menuItems\n * @param {object} menuItem\n * @returns {boolean}\n */\nfunction isChildMenuItem(menuItems, menuItem) {\n return menuItems.some((candidate) => candidate.children?.includes(menuItem));\n}\n\n/**\n * La entrada del menú de la pantalla abierta: la más específica gana (/orders/15 marca «Pedidos», no «Inicio»).\n *\n * @param {Array<{to: string, matchPrefix?: string}>} menuItems lo que regresa composeMenu\n * @param {string} pathname location.pathname\n * @returns {object|null}\n */\nexport function activeMenuItemOf(menuItems, pathname) {\n const matchingItems = screenMenuItemsOf(menuItems).filter((menuItem) => {\n const prefix = menuItem.matchPrefix ?? menuItem.to;\n // el inicio solo se marca en el inicio: todas las direcciones empiezan con '/'\n const isHome = prefix === HOME_PATH;\n return pathname === prefix || (!isHome && pathname.startsWith(`${prefix}/`));\n });\n matchingItems.sort((first, second) => {\n const lengthDifference = (second.matchPrefix ?? second.to).length - (first.matchPrefix ?? first.to).length;\n if (lengthDifference !== 0) {\n return lengthDifference;\n }\n // con el mismo camino (un submenú cuya primera hija lleva a donde mismo que él), gana la hija: es la pantalla\n return Number(isChildMenuItem(menuItems, second)) - Number(isChildMenuItem(menuItems, first));\n });\n return matchingItems[0] ?? null;\n}\n\n/**\n * El submenú que contiene a una entrada (para abrirlo y para las migas); null si la entrada está arriba.\n *\n * @param {object[]} menuItems lo que regresa composeMenu\n * @param {object|null} menuItem\n * @returns {object|null}\n */\nexport function parentMenuItemOf(menuItems, menuItem) {\n return menuItems.find((candidate) => candidate.children?.includes(menuItem)) ?? null;\n}\n\n/**\n * Las migas de la pantalla abierta, sacadas del menú: el inicio, el submenú (si lo hay) y la entrada activa.\n * En el inicio, o en una pantalla que no está en el menú, regresa solo lo que encuentra (una o ninguna).\n *\n * @param {object[]} menuItems lo que regresa composeMenu\n * @param {string} pathname location.pathname\n * @returns {object[]} entradas del menú, en orden\n */\nexport function menuBreadcrumbsOf(menuItems, pathname) {\n const activeItem = activeMenuItemOf(menuItems, pathname);\n if (!activeItem) {\n return [];\n }\n const homeItem = menuItems.find((menuItem) => menuItem.to === HOME_PATH) ?? null;\n const parentItem = parentMenuItemOf(menuItems, activeItem);\n return [homeItem, parentItem, activeItem].filter((menuItem, index, crumbs) => Boolean(menuItem) && crumbs.indexOf(menuItem) === index);\n}\n\n/**\n * El badge de una entrada del menú: el conteo que llegó del servidor por su badgeKey (lo pendiente de esa pantalla), la\n * suma de sus hijas si es un submenú (como las carpetas del iPhone), o el badge fijo de su menu.js («Nuevo»). Un conteo\n * en cero no se enseña: no hay nada pendiente.\n *\n * menuBadgeOf({badgeKey: 'pendingPayment'}, {pendingPayment: 13}) // {content: 13, isCount: true}\n *\n * @param {object} menuItem una entrada de composeMenu\n * @param {Record<string, number>} [menuBadges] los conteos por badgeKey (el proyecto los pide a su servidor)\n * @returns {{content: number|string, isCount: boolean}|null}\n */\nexport function menuBadgeOf(menuItem, menuBadges = {}) {\n const countOf = (item) => {\n const count = item.badgeKey ? Number(menuBadges[item.badgeKey] ?? 0) : 0;\n return Number.isFinite(count) && count > 0 ? count : 0;\n };\n const ownCount = countOf(menuItem);\n const childrenCount = Array.isArray(menuItem.children) ? menuItem.children.reduce((total, child) => total + countOf(child), 0) : 0;\n const total = ownCount + childrenCount;\n if (total > 0) {\n return {content: total, isCount: true};\n }\n if (menuItem.badge !== undefined && menuItem.badge !== null && menuItem.badge !== '') {\n return {content: menuItem.badge, isCount: typeof menuItem.badge === 'number'};\n }\n return null;\n}\n\n// cuántas pestañas van en la barra del teléfono si no se dice otra cosa, como las de Apple: hasta 5; si hay más entradas,\n// 4 y «Más» con el resto. La barra (MobileTabBar) pasa las que de verdad caben a lo ancho\nconst MOBILE_TAB_LIMIT = 5;\n/** Cuántas puede fijar la persona en la barra: las que caben junto a «Más». */\nexport const PINNABLE_TAB_LIMIT = MOBILE_TAB_LIMIT - 1;\n\n/**\n * Con qué se reconoce una entrada del menú para recordarla (las pestañas que fijó la persona): su ruta, o su llave de\n * texto si no navega. No el texto traducido: cambia con el idioma.\n *\n * @param {{to?: string, labelKey?: string, label?: string}} menuItem\n * @returns {string}\n */\nexport function menuItemKeyOf(menuItem) {\n return menuItem.to ?? menuItem.labelKey ?? menuItem.label;\n}\n\n/**\n * Las pestañas de la barra del teléfono y lo que va en «Más». Van las que fijó la persona (pinnedKeys, en el orden del\n * menú); si no fijó ninguna, las que el menu.js marca con mobileTab: true; si ninguna lo dice, las primeras. Si todas\n * las entradas caben (tabLimit), van todas y no hay «Más»; si no, tabLimit - 1 y «Más» con el resto.\n *\n * @param {object[]} menuItems lo que regresa composeMenu\n * @param {string[]|null} [pinnedKeys] las que eligió la persona (menuItemKeyOf); las que ya no existen o ya no puede ver\n * no cuentan\n * @param {number} [tabLimit] cuántas caben en la barra contando «Más» (por omisión 5; la barra mide su ancho)\n * @returns {{tabs: object[], more: object[]}}\n */\nexport function mobileTabsOf(menuItems, pinnedKeys = null, tabLimit = MOBILE_TAB_LIMIT) {\n if (menuItems.length <= tabLimit) {\n return {tabs: menuItems, more: []};\n }\n const pinned = menuItems.filter((menuItem) => (pinnedKeys ?? []).includes(menuItemKeyOf(menuItem)));\n const marked = menuItems.filter((menuItem) => menuItem.mobileTab);\n let candidates = menuItems;\n if (pinned.length > 0) {\n candidates = pinned;\n } else if (marked.length > 0) {\n candidates = marked;\n }\n // una de las que caben es para «Más»\n const tabs = candidates.slice(0, Math.max(tabLimit - 1, 1));\n return {tabs, more: menuItems.filter((menuItem) => !tabs.includes(menuItem))};\n}\n"],"names":["total"],"mappings":"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"}
@@ -17,10 +17,27 @@ function currentLevel() {
17
17
  }
18
18
  return defaultLevel;
19
19
  }
20
+ const sinks = [];
21
+ function addLogSink(sink) {
22
+ sinks.push(sink);
23
+ return () => {
24
+ const position = sinks.indexOf(sink);
25
+ if (position !== -1) {
26
+ sinks.splice(position, 1);
27
+ }
28
+ };
29
+ }
20
30
  function emit(level, namespace, message, context) {
21
31
  if (LEVELS[level] > currentLevel()) {
22
32
  return;
23
33
  }
34
+ const entry = { level, namespace, message, ...context !== void 0 && { context }, timestamp: Date.now() };
35
+ for (const sink of sinks) {
36
+ try {
37
+ sink(entry);
38
+ } catch {
39
+ }
40
+ }
24
41
  const write = level === "debug" ? console.log : console[level] ?? console.log;
25
42
  if (context === void 0) {
26
43
  write(`[${namespace}] ${message}`);
@@ -37,6 +54,7 @@ function createLogger(namespace) {
37
54
  };
38
55
  }
39
56
  export {
57
+ addLogSink,
40
58
  createLogger,
41
59
  setLogLevel
42
60
  };
@@ -1 +1 @@
1
- {"version":3,"file":"logger.js","sources":["../../src/observability/logger.js"],"sourcesContent":["// Logger con espacio de nombres sobre console.\n//\n// Por qué un envoltorio y no una librería: corre en el navegador, son pocas líneas legibles y, cuando haga falta un\n// reporte externo (Sentry o el que sea), se enchufa en emit() sin tocar cada llamada.\n//\n// Reglas:\n// - datos estructurados: log.warn('mensaje', {status, requestId}); nunca concatenar cadenas.\n// - IMPORTANTE: sin datos personales. Nada de correos, nombres, contraseñas ni contenido de archivos: solo ids,\n// códigos, status y el request_id (misma regla que el backend: logs sin datos personales).\n// - por omisión solo salen error y warn. La aplicación pide todo en desarrollo con setLogLevel('debug') (el paquete no\n// sabe si la aplicación corre en desarrollo: import.meta.env es de quien construye). Para ver más en una instalación\n// ya desplegada:\n// localStorage.setItem('calcita:logLevel', 'debug')\n//\n// Funciones puras, no renderizan: function (regla de la casa).\n\nconst LEVELS = {error: 0, warn: 1, info: 2, debug: 3};\n\nconst LOG_LEVEL_STORAGE_KEY = 'calcita:logLevel';\n\n// el nivel cuando nadie lo pidió en localStorage; lo cambia la aplicación con setLogLevel\nlet defaultLevel = LEVELS.warn;\n\n/**\n * El nivel de lo que sale en consola: 'error', 'warn', 'info' o 'debug'. Lo llama la aplicación al arrancar, por ejemplo\n * setLogLevel(import.meta.env.DEV ? 'debug' : 'warn').\n *\n * @param {'error'|'warn'|'info'|'debug'} level\n * @returns {void}\n */\nexport function setLogLevel(level) {\n if (!(level in LEVELS)) {\n throw new Error(`setLogLevel espera error, warn, info o debug y recibió «${level}»`);\n }\n defaultLevel = LEVELS[level];\n}\n\nfunction currentLevel() {\n try {\n const override = window.localStorage?.getItem(LOG_LEVEL_STORAGE_KEY);\n if (override && override in LEVELS) {\n return LEVELS[override];\n }\n } catch {\n // ventana privada o almacenamiento bloqueado: usamos el nivel por defecto\n }\n return defaultLevel;\n}\n\nfunction emit(level, namespace, message, context) {\n if (LEVELS[level] > currentLevel()) {\n return;\n }\n\n // OJO: debug sale por console.log a propósito; console.debug cae en \"Verbose\" y DevTools lo esconde\n const write = level === 'debug' ? console.log : (console[level] ?? console.log);\n if (context === undefined) {\n write(`[${namespace}] ${message}`);\n } else {\n write(`[${namespace}] ${message}`, context);\n }\n}\n\n/**\n * Logger que antepone [namespace] a cada mensaje.\n *\n * @param {string} namespace p. ej. 'api', 'session'\n */\nexport function createLogger(namespace) {\n return {\n error: (message, context) => emit('error', namespace, message, context),\n warn: (message, context) => emit('warn', namespace, message, context),\n info: (message, context) => emit('info', namespace, message, context),\n debug: (message, context) => emit('debug', namespace, message, context),\n };\n}\n"],"names":[],"mappings":"AAgBA,MAAM,SAAS,EAAC,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,EAAA;AAEnD,MAAM,wBAAwB;AAG9B,IAAI,eAAe,OAAO;AASnB,SAAS,YAAY,OAAO;AAC/B,MAAI,EAAE,SAAS,SAAS;AACpB,UAAM,IAAI,MAAM,2DAA2D,KAAK,GAAG;AAAA,EACvF;AACA,iBAAe,OAAO,KAAK;AAC/B;AAEA,SAAS,eAAe;AACpB,MAAI;AACA,UAAM,WAAW,OAAO,cAAc,QAAQ,qBAAqB;AACnE,QAAI,YAAY,YAAY,QAAQ;AAChC,aAAO,OAAO,QAAQ;AAAA,IAC1B;AAAA,EACJ,QAAQ;AAAA,EAER;AACA,SAAO;AACX;AAEA,SAAS,KAAK,OAAO,WAAW,SAAS,SAAS;AAC9C,MAAI,OAAO,KAAK,IAAI,gBAAgB;AAChC;AAAA,EACJ;AAGA,QAAM,QAAQ,UAAU,UAAU,QAAQ,MAAO,QAAQ,KAAK,KAAK,QAAQ;AAC3E,MAAI,YAAY,QAAW;AACvB,UAAM,IAAI,SAAS,KAAK,OAAO,EAAE;AAAA,EACrC,OAAO;AACH,UAAM,IAAI,SAAS,KAAK,OAAO,IAAI,OAAO;AAAA,EAC9C;AACJ;AAOO,SAAS,aAAa,WAAW;AACpC,SAAO;AAAA,IACH,OAAO,CAAC,SAAS,YAAY,KAAK,SAAS,WAAW,SAAS,OAAO;AAAA,IACtE,MAAM,CAAC,SAAS,YAAY,KAAK,QAAQ,WAAW,SAAS,OAAO;AAAA,IACpE,MAAM,CAAC,SAAS,YAAY,KAAK,QAAQ,WAAW,SAAS,OAAO;AAAA,IACpE,OAAO,CAAC,SAAS,YAAY,KAAK,SAAS,WAAW,SAAS,OAAO;AAAA,EAAA;AAE9E;"}
1
+ {"version":3,"file":"logger.js","sources":["../../src/observability/logger.js"],"sourcesContent":["// Logger con espacio de nombres sobre console.\n//\n// Por qué un envoltorio y no una librería: corre en el navegador, son pocas líneas legibles y un reporte externo (Sentry,\n// Datadog, un endpoint propio) se enchufa con addLogSink sin tocar cada llamada:\n// addLogSink((entry) => Sentry.logger[entry.level](`[${entry.namespace}] ${entry.message}`, entry.context));\n// Cada salida recibe lo mismo que sale en consola (respeta el nivel); una salida que truena no tumba la app.\n//\n// Reglas:\n// - datos estructurados: log.warn('mensaje', {status, requestId}); nunca concatenar cadenas.\n// - IMPORTANTE: sin datos personales. Nada de correos, nombres, contraseñas ni contenido de archivos: solo ids,\n// códigos, status y el request_id (misma regla que el backend: logs sin datos personales).\n// - por omisión solo salen error y warn. La aplicación pide todo en desarrollo con setLogLevel('debug') (el paquete no\n// sabe si la aplicación corre en desarrollo: import.meta.env es de quien construye). Para ver más en una instalación\n// ya desplegada:\n// localStorage.setItem('calcita:logLevel', 'debug')\n//\n// Funciones puras, no renderizan: function (regla de la casa).\n\nconst LEVELS = {error: 0, warn: 1, info: 2, debug: 3};\n\nconst LOG_LEVEL_STORAGE_KEY = 'calcita:logLevel';\n\n// el nivel cuando nadie lo pidió en localStorage; lo cambia la aplicación con setLogLevel\nlet defaultLevel = LEVELS.warn;\n\n/**\n * El nivel de lo que sale en consola: 'error', 'warn', 'info' o 'debug'. Lo llama la aplicación al arrancar, por ejemplo\n * setLogLevel(import.meta.env.DEV ? 'debug' : 'warn').\n *\n * @param {'error'|'warn'|'info'|'debug'} level\n * @returns {void}\n */\nexport function setLogLevel(level) {\n if (!(level in LEVELS)) {\n throw new Error(`setLogLevel espera error, warn, info o debug y recibió «${level}»`);\n }\n defaultLevel = LEVELS[level];\n}\n\nfunction currentLevel() {\n try {\n const override = window.localStorage?.getItem(LOG_LEVEL_STORAGE_KEY);\n if (override && override in LEVELS) {\n return LEVELS[override];\n }\n } catch {\n // ventana privada o almacenamiento bloqueado: usamos el nivel por defecto\n }\n return defaultLevel;\n}\n\n// las salidas propias del proyecto (addLogSink), además de la consola\nconst sinks = [];\n\n/**\n * Una salida propia para lo que se anota (un reporte externo): recibe cada anotación que sale en consola, con su nivel.\n *\n * @param {(entry: {level: 'error'|'warn'|'info'|'debug', namespace: string, message: string, context?: object, timestamp: number}) => void} sink\n * @returns {() => void} para quitarla\n */\nexport function addLogSink(sink) {\n sinks.push(sink);\n return () => {\n const position = sinks.indexOf(sink);\n if (position !== -1) {\n sinks.splice(position, 1);\n }\n };\n}\n\nfunction emit(level, namespace, message, context) {\n if (LEVELS[level] > currentLevel()) {\n return;\n }\n const entry = {level, namespace, message, ...(context !== undefined && {context}), timestamp: Date.now()};\n for (const sink of sinks) {\n try {\n sink(entry);\n } catch {\n // una salida con un error no debe tumbar la app ni la anotación en consola\n }\n }\n\n // OJO: debug sale por console.log a propósito; console.debug cae en \"Verbose\" y DevTools lo esconde\n const write = level === 'debug' ? console.log : (console[level] ?? console.log);\n if (context === undefined) {\n write(`[${namespace}] ${message}`);\n } else {\n write(`[${namespace}] ${message}`, context);\n }\n}\n\n/**\n * Logger que antepone [namespace] a cada mensaje.\n *\n * @param {string} namespace p. ej. 'api', 'session'\n */\nexport function createLogger(namespace) {\n return {\n error: (message, context) => emit('error', namespace, message, context),\n warn: (message, context) => emit('warn', namespace, message, context),\n info: (message, context) => emit('info', namespace, message, context),\n debug: (message, context) => emit('debug', namespace, message, context),\n };\n}\n"],"names":[],"mappings":"AAkBA,MAAM,SAAS,EAAC,OAAO,GAAG,MAAM,GAAG,MAAM,GAAG,OAAO,EAAA;AAEnD,MAAM,wBAAwB;AAG9B,IAAI,eAAe,OAAO;AASnB,SAAS,YAAY,OAAO;AAC/B,MAAI,EAAE,SAAS,SAAS;AACpB,UAAM,IAAI,MAAM,2DAA2D,KAAK,GAAG;AAAA,EACvF;AACA,iBAAe,OAAO,KAAK;AAC/B;AAEA,SAAS,eAAe;AACpB,MAAI;AACA,UAAM,WAAW,OAAO,cAAc,QAAQ,qBAAqB;AACnE,QAAI,YAAY,YAAY,QAAQ;AAChC,aAAO,OAAO,QAAQ;AAAA,IAC1B;AAAA,EACJ,QAAQ;AAAA,EAER;AACA,SAAO;AACX;AAGA,MAAM,QAAQ,CAAA;AAQP,SAAS,WAAW,MAAM;AAC7B,QAAM,KAAK,IAAI;AACf,SAAO,MAAM;AACT,UAAM,WAAW,MAAM,QAAQ,IAAI;AACnC,QAAI,aAAa,IAAI;AACjB,YAAM,OAAO,UAAU,CAAC;AAAA,IAC5B;AAAA,EACJ;AACJ;AAEA,SAAS,KAAK,OAAO,WAAW,SAAS,SAAS;AAC9C,MAAI,OAAO,KAAK,IAAI,gBAAgB;AAChC;AAAA,EACJ;AACA,QAAM,QAAQ,EAAC,OAAO,WAAW,SAAS,GAAI,YAAY,UAAa,EAAC,QAAA,GAAW,WAAW,KAAK,MAAI;AACvG,aAAW,QAAQ,OAAO;AACtB,QAAI;AACA,WAAK,KAAK;AAAA,IACd,QAAQ;AAAA,IAER;AAAA,EACJ;AAGA,QAAM,QAAQ,UAAU,UAAU,QAAQ,MAAO,QAAQ,KAAK,KAAK,QAAQ;AAC3E,MAAI,YAAY,QAAW;AACvB,UAAM,IAAI,SAAS,KAAK,OAAO,EAAE;AAAA,EACrC,OAAO;AACH,UAAM,IAAI,SAAS,KAAK,OAAO,IAAI,OAAO;AAAA,EAC9C;AACJ;AAOO,SAAS,aAAa,WAAW;AACpC,SAAO;AAAA,IACH,OAAO,CAAC,SAAS,YAAY,KAAK,SAAS,WAAW,SAAS,OAAO;AAAA,IACtE,MAAM,CAAC,SAAS,YAAY,KAAK,QAAQ,WAAW,SAAS,OAAO;AAAA,IACpE,MAAM,CAAC,SAAS,YAAY,KAAK,QAAQ,WAAW,SAAS,OAAO;AAAA,IACpE,OAAO,CAAC,SAAS,YAAY,KAAK,SAAS,WAAW,SAAS,OAAO;AAAA,EAAA;AAE9E;"}
@@ -1,4 +1,5 @@
1
1
  import { paletteOf } from "./paletteOf.js";
2
+ import { lightDarkStylesOf } from "../lightDarkStyles.js";
2
3
  function MuiAppBar() {
3
4
  return {
4
5
  MuiAppBar: {
@@ -9,11 +10,11 @@ function MuiAppBar() {
9
10
  const isPinned = ownerState.position === "sticky" || ownerState.position === "fixed";
10
11
  const isPrimary = (ownerState.color ?? "primary") === "primary";
11
12
  return {
12
- ...isPrimary && {
13
- backgroundColor: palette.primary.main,
14
- color: palette.primary.contrastText,
15
- ...theme.applyStyles("dark", { backgroundColor: palette.background.paper, color: palette.text.primary })
16
- },
13
+ ...isPrimary && lightDarkStylesOf(
14
+ theme,
15
+ { backgroundColor: palette.primary.main, color: palette.primary.contrastText },
16
+ { backgroundColor: palette.background.paper, color: palette.text.primary }
17
+ ),
17
18
  ...isPinned && { borderBottom: `1px solid ${palette.divider}` }
18
19
  };
19
20
  }
@@ -1 +1 @@
1
- {"version":3,"file":"MuiAppBar.js","sources":["../../../src/theme/components/MuiAppBar.js"],"sourcesContent":["// La barra de arriba: plana (sin sombra), naranja en claro y del color de las tarjetas en oscuro, con una línea abajo\n// cuando se queda fija al deslizar.\n//\n// Por qué applyStyles('dark') y no theme.palette.mode: el servidor no sabe qué modo eligió la persona; con mode, la barra\n// se pintaba naranja un instante en oscuro. applyStyles lo deja en CSS y el navegador elige antes de pintar.\n// Solo cambia la barra de color principal (el de omisión): una barra con color=\"inherit\" o \"transparent\" se respeta.\n//\n// Funciones puras, sin React: function (regla de la casa).\nimport {paletteOf} from '@/theme/components/paletteOf.js';\n\nexport function MuiAppBar() {\n return {\n MuiAppBar: {\n defaultProps: {elevation: 0},\n styleOverrides: {\n root: ({ownerState, theme}) => {\n const palette = paletteOf(theme);\n const isPinned = ownerState.position === 'sticky' || ownerState.position === 'fixed';\n const isPrimary = (ownerState.color ?? 'primary') === 'primary';\n return {\n ...(isPrimary && {\n backgroundColor: palette.primary.main,\n color: palette.primary.contrastText,\n ...theme.applyStyles('dark', {backgroundColor: palette.background.paper, color: palette.text.primary}),\n }),\n ...(isPinned && {borderBottom: `1px solid ${palette.divider}`}),\n };\n },\n },\n },\n };\n}\n"],"names":[],"mappings":";AAUO,SAAS,YAAY;AACxB,SAAO;AAAA,IACH,WAAW;AAAA,MACP,cAAc,EAAC,WAAW,EAAC;AAAA,MAC3B,gBAAgB;AAAA,QACZ,MAAM,CAAC,EAAC,YAAY,MAAK,MAAM;AAC3B,gBAAM,UAAU,UAAU,KAAK;AAC/B,gBAAM,WAAW,WAAW,aAAa,YAAY,WAAW,aAAa;AAC7E,gBAAM,aAAa,WAAW,SAAS,eAAe;AACtD,iBAAO;AAAA,YACH,GAAI,aAAa;AAAA,cACb,iBAAiB,QAAQ,QAAQ;AAAA,cACjC,OAAO,QAAQ,QAAQ;AAAA,cACvB,GAAG,MAAM,YAAY,QAAQ,EAAC,iBAAiB,QAAQ,WAAW,OAAO,OAAO,QAAQ,KAAK,QAAO,CAAC;AAAA,YACjI;AAAA,YACwB,GAAI,YAAY,EAAC,cAAc,aAAa,QAAQ,OAAO,GAAE;AAAA,UACrF;AAAA,QACgB;AAAA,MAChB;AAAA,IACA;AAAA,EACA;AACA;"}
1
+ {"version":3,"file":"MuiAppBar.js","sources":["../../../src/theme/components/MuiAppBar.js"],"sourcesContent":["// La barra de arriba: plana (sin sombra), naranja en claro y del color de las tarjetas en oscuro, con una línea abajo\n// cuando se queda fija al deslizar.\n//\n// Por qué el modo va en CSS (lightDarkStylesOf) y no en theme.palette.mode: el servidor no sabe qué modo eligió la persona;\n// con mode, la barra se pintaba naranja un instante en oscuro. En CSS el navegador elige antes de pintar.\n// Solo cambia la barra de color principal (el de omisión): una barra con color=\"inherit\" o \"transparent\" se respeta.\n//\n// Funciones puras, sin React: function (regla de la casa).\nimport {paletteOf} from '@/theme/components/paletteOf.js';\nimport {lightDarkStylesOf} from '@/theme/lightDarkStyles.js';\n\nexport function MuiAppBar() {\n return {\n MuiAppBar: {\n defaultProps: {elevation: 0},\n styleOverrides: {\n root: ({ownerState, theme}) => {\n const palette = paletteOf(theme);\n const isPinned = ownerState.position === 'sticky' || ownerState.position === 'fixed';\n const isPrimary = (ownerState.color ?? 'primary') === 'primary';\n return {\n ...(isPrimary && lightDarkStylesOf(theme,\n {backgroundColor: palette.primary.main, color: palette.primary.contrastText},\n {backgroundColor: palette.background.paper, color: palette.text.primary})),\n ...(isPinned && {borderBottom: `1px solid ${palette.divider}`}),\n };\n },\n },\n },\n };\n}\n"],"names":[],"mappings":";;AAWO,SAAS,YAAY;AACxB,SAAO;AAAA,IACH,WAAW;AAAA,MACP,cAAc,EAAC,WAAW,EAAC;AAAA,MAC3B,gBAAgB;AAAA,QACZ,MAAM,CAAC,EAAC,YAAY,MAAK,MAAM;AAC3B,gBAAM,UAAU,UAAU,KAAK;AAC/B,gBAAM,WAAW,WAAW,aAAa,YAAY,WAAW,aAAa;AAC7E,gBAAM,aAAa,WAAW,SAAS,eAAe;AACtD,iBAAO;AAAA,YACH,GAAI,aAAa;AAAA,cAAkB;AAAA,cAC/B,EAAC,iBAAiB,QAAQ,QAAQ,MAAM,OAAO,QAAQ,QAAQ,aAAY;AAAA,cAC3E,EAAC,iBAAiB,QAAQ,WAAW,OAAO,OAAO,QAAQ,KAAK,QAAO;AAAA,YAAC;AAAA,YAC5E,GAAI,YAAY,EAAC,cAAc,aAAa,QAAQ,OAAO,GAAE;AAAA,UACrF;AAAA,QACgB;AAAA,MAChB;AAAA,IACA;AAAA,EACA;AACA;"}
@@ -1,5 +1,6 @@
1
1
  import { GLASS_MENU_RADIUS_PIXELS, GLASS_PANEL_RADIUS_PIXELS, GLASS_INSET_PIXELS, solidFallbackOf, glassDropOf, accentTextStylesOf, glassStylesOf, insideContentSelectorOf, canvasFieldStylesOf, GLASS_LENS_FLAG_SELECTOR, glassSurfaceOf, materialStylesOf, whiteLabelFillOf, fieldWellStylesOf, glassOpacityOf } from "../glassSurface.js";
2
2
  import { mergeStyles } from "../mergeComponents.js";
3
+ import { lightDarkStylesOf } from "../lightDarkStyles.js";
3
4
  import { textSizeOf } from "../appleTracking.js";
4
5
  import { paletteOf } from "./paletteOf.js";
5
6
  const MENU_ROW_INSET_PIXELS = 4;
@@ -100,14 +101,10 @@ function whiteLabelOf(theme, colorName) {
100
101
  "&:hover": { backgroundColor: `color-mix(in srgb, ${fill} ${100 - PROMINENT_HOVER_SHADE_PERCENT}%, black)` }
101
102
  };
102
103
  };
103
- return { ...schemeStylesOf("light"), ...theme.applyStyles("dark", schemeStylesOf("dark")) };
104
+ return lightDarkStylesOf(theme, schemeStylesOf("light"), schemeStylesOf("dark"));
104
105
  }
105
106
  function monogramOf(theme) {
106
- return {
107
- color: WHITE_LABEL,
108
- backgroundColor: MONOGRAM_GRAY.light,
109
- ...theme.applyStyles("dark", { color: WHITE_LABEL, backgroundColor: MONOGRAM_GRAY.dark })
110
- };
107
+ return lightDarkStylesOf(theme, { color: WHITE_LABEL, backgroundColor: MONOGRAM_GRAY.light }, { color: WHITE_LABEL, backgroundColor: MONOGRAM_GRAY.dark });
111
108
  }
112
109
  function tintedControlOf(theme, toneColor) {
113
110
  const fillOf = (percent) => `color-mix(in srgb, ${toneColor} ${percent}%, transparent)`;
@@ -115,12 +112,11 @@ function tintedControlOf(theme, toneColor) {
115
112
  return {
116
113
  ...plain,
117
114
  color: paletteOf(theme).text.primary,
118
- backgroundColor: fillOf(TINTED_FILL_PERCENT.light.rest),
119
- "&:hover": { ...plain, backgroundColor: fillOf(TINTED_FILL_PERCENT.light.hover) },
120
- ...theme.applyStyles("dark", {
121
- backgroundColor: fillOf(TINTED_FILL_PERCENT.dark.rest),
122
- "&:hover": { backgroundColor: fillOf(TINTED_FILL_PERCENT.dark.hover) }
123
- })
115
+ ...lightDarkStylesOf(
116
+ theme,
117
+ { backgroundColor: fillOf(TINTED_FILL_PERCENT.light.rest), "&:hover": { ...plain, backgroundColor: fillOf(TINTED_FILL_PERCENT.light.hover) } },
118
+ { backgroundColor: fillOf(TINTED_FILL_PERCENT.dark.rest), "&:hover": { backgroundColor: fillOf(TINTED_FILL_PERCENT.dark.hover) } }
119
+ )
124
120
  };
125
121
  }
126
122
  function fieldFillOf(theme) {
@@ -359,11 +355,11 @@ function glassComponents({ surfaces = false } = {}) {
359
355
  return {
360
356
  ...surface,
361
357
  // de color: su vidrio lleva el color oscurecido lo justo para la letra blanca (whiteLabelFillOf)
362
- ...lightFill && { ...glassSurfaceOf(lightFill, { opacity: PROMINENT_GLASS_OPACITY }), color: WHITE_LABEL },
363
- ...darkFill && theme.applyStyles("dark", {
364
- ...glassSurfaceOf(darkFill, { opacity: PROMINENT_GLASS_OPACITY, isDark: true }),
365
- color: WHITE_LABEL
366
- }),
358
+ ...lightDarkStylesOf(
359
+ theme,
360
+ lightFill ? { ...glassSurfaceOf(lightFill, { opacity: PROMINENT_GLASS_OPACITY }), color: WHITE_LABEL } : {},
361
+ darkFill ? { ...glassSurfaceOf(darkFill, { opacity: PROMINENT_GLASS_OPACITY, isDark: true }), color: WHITE_LABEL } : {}
362
+ ),
367
363
  transition: theme.transitions.create(["background-color", "box-shadow", "transform"], { duration: theme.transitions.duration.short }),
368
364
  // MUI cambia el fondo y la sombra al pasar y al apretar: el vidrio se queda, y al apretar se hunde
369
365
  "&:hover": { backgroundColor: surface.backgroundColor },
@@ -463,8 +459,11 @@ function glassComponents({ surfaces = false } = {}) {
463
459
  overflow: "hidden",
464
460
  [insideContentSelectorOf()]: {
465
461
  ...plain,
466
- backgroundColor: groupFillOf(TINTED_FILL_PERCENT.light.rest),
467
- ...theme.applyStyles("dark", { backgroundColor: groupFillOf(TINTED_FILL_PERCENT.dark.rest) })
462
+ ...lightDarkStylesOf(
463
+ theme,
464
+ { backgroundColor: groupFillOf(TINTED_FILL_PERCENT.light.rest) },
465
+ { backgroundColor: groupFillOf(TINTED_FILL_PERCENT.dark.rest) }
466
+ )
468
467
  }
469
468
  };
470
469
  },
@@ -644,8 +643,11 @@ function glassComponents({ surfaces = false } = {}) {
644
643
  const whiteMark = { background: "linear-gradient(#fff, #fff) center / 58% 58% no-repeat", borderRadius: 4 };
645
644
  return {
646
645
  "&.Mui-checked .MuiSvgIcon-root, &.MuiCheckbox-indeterminate .MuiSvgIcon-root": whiteMark,
647
- ...lightFill && { "&.Mui-checked, &.MuiCheckbox-indeterminate": { color: lightFill } },
648
- ...darkFill && theme.applyStyles("dark", { "&.Mui-checked, &.MuiCheckbox-indeterminate": { color: darkFill } })
646
+ ...lightDarkStylesOf(
647
+ theme,
648
+ lightFill ? { "&.Mui-checked, &.MuiCheckbox-indeterminate": { color: lightFill } } : {},
649
+ darkFill ? { "&.Mui-checked, &.MuiCheckbox-indeterminate": { color: darkFill } } : {}
650
+ )
649
651
  };
650
652
  }
651
653
  }
@@ -671,13 +673,12 @@ function glassComponents({ surfaces = false } = {}) {
671
673
  const lightFill = fillOf("light");
672
674
  const darkFill = fillOf("dark");
673
675
  return {
674
- color: STEP_PENDING_GRAY.light,
675
676
  "& .MuiStepIcon-text": { fill: WHITE_LABEL },
676
- ...lightFill && { "&.Mui-active, &.Mui-completed": { color: lightFill } },
677
- ...theme.applyStyles("dark", {
678
- color: STEP_PENDING_GRAY.dark,
679
- ...darkFill && { "&.Mui-active, &.Mui-completed": { color: darkFill } }
680
- })
677
+ ...lightDarkStylesOf(
678
+ theme,
679
+ { color: STEP_PENDING_GRAY.light, ...lightFill && { "&.Mui-active, &.Mui-completed": { color: lightFill } } },
680
+ { color: STEP_PENDING_GRAY.dark, ...darkFill && { "&.Mui-active, &.Mui-completed": { color: darkFill } } }
681
+ )
681
682
  };
682
683
  }
683
684
  }
@@ -803,9 +804,8 @@ function glassComponents({ surfaces = false } = {}) {
803
804
  }),
804
805
  track: ({ theme }) => ({
805
806
  opacity: 1,
806
- backgroundColor: SWITCH_OFF_TRACK.light,
807
807
  transition: theme.transitions.create(["background-color"], { duration: theme.transitions.duration.short }),
808
- ...theme.applyStyles("dark", { backgroundColor: SWITCH_OFF_TRACK.dark })
808
+ ...lightDarkStylesOf(theme, { backgroundColor: SWITCH_OFF_TRACK.light }, { backgroundColor: SWITCH_OFF_TRACK.dark })
809
809
  })
810
810
  }
811
811
  },
@@ -841,8 +841,7 @@ function glassComponents({ surfaces = false } = {}) {
841
841
  },
842
842
  rail: ({ theme }) => ({
843
843
  opacity: 1,
844
- backgroundColor: SLIDER_RAIL.light,
845
- ...theme.applyStyles("dark", { backgroundColor: SLIDER_RAIL.dark })
844
+ ...lightDarkStylesOf(theme, { backgroundColor: SLIDER_RAIL.light }, { backgroundColor: SLIDER_RAIL.dark })
846
845
  }),
847
846
  root: ({ ownerState }) => ownerState.orientation === "vertical" ? { width: SLIDER_TRACK_PIXELS } : { height: SLIDER_TRACK_PIXELS }
848
847
  }
@@ -880,6 +879,8 @@ function glassComponents({ surfaces = false } = {}) {
880
879
  };
881
880
  }
882
881
  export {
882
+ MONOGRAM_GRAY,
883
+ WHITE_LABEL,
883
884
  glassComponents
884
885
  };
885
886
  //# sourceMappingURL=glassComponents.js.map