@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
@@ -15,6 +15,7 @@ import { useTheme } from "@mui/material/styles";
15
15
  import KeyboardArrowDownRoundedIcon from "@mui/icons-material/KeyboardArrowDownRounded";
16
16
  import KeyboardArrowUpRoundedIcon from "@mui/icons-material/KeyboardArrowUpRounded";
17
17
  import { menuSectionsOf, menuItemLabelOf } from "./menuItemText.js";
18
+ import Wordmark from "./Wordmark.js";
18
19
  import { APP_BAR_HEIGHT_PIXELS } from "./shellSizes.js";
19
20
  import SideMenuEntry from "./SideMenuEntry.js";
20
21
  import { isGlassTheme, glassStylesOf } from "../theme/glassSurface.js";
@@ -47,6 +48,7 @@ const BRAND_LEFT_PADDING = "18px";
47
48
  const SECTION_TITLE_FONT_REM = Object.freeze({ solid: 0.6875, glass: 0.75 });
48
49
  function SideMenu({
49
50
  logo,
51
+ wordmark,
50
52
  brandName,
51
53
  brandCaption,
52
54
  homePath,
@@ -93,7 +95,7 @@ function SideMenu({
93
95
  // crecía sin límite y la hoja no se deslizaba (lo reportaron con el teléfono acostado)
94
96
  /* @__PURE__ */ jsxs(Box, { sx: { display: "flex", flexDirection: "column", flex: "1 1 auto", minHeight: 0 }, children: [
95
97
  showBrand && /* @__PURE__ */ jsxs(Fragment, { children: [
96
- /* @__PURE__ */ jsxs(
98
+ /* @__PURE__ */ jsx(
97
99
  ButtonBase,
98
100
  {
99
101
  component: RouterLink,
@@ -110,13 +112,13 @@ function SideMenu({
110
112
  pr: 2,
111
113
  flexShrink: 0
112
114
  },
113
- children: [
115
+ children: wordmark ? /* @__PURE__ */ jsx(Wordmark, { children: wordmark }) : /* @__PURE__ */ jsxs(Fragment, { children: [
114
116
  /* @__PURE__ */ jsx(Box, { sx: { width: 28, height: 28, flex: "0 0 auto", display: "flex", alignItems: "center", justifyContent: "center" }, children: logo }),
115
117
  /* @__PURE__ */ jsxs(Box, { sx: (theme2) => ({ minWidth: 0, textAlign: "left", ...fadeWhenCollapsed(theme2) }), children: [
116
118
  /* @__PURE__ */ jsx(Typography, { variant: "subtitle2", component: "span", noWrap: true, sx: { display: "block", fontWeight: 600, lineHeight: 1.3 }, children: brandName }),
117
119
  brandCaption && /* @__PURE__ */ jsx(Typography, { variant: "caption", component: "span", color: "text.secondary", noWrap: true, sx: { display: "block", lineHeight: 1.3 }, children: brandCaption })
118
120
  ] })
119
- ]
121
+ ] })
120
122
  }
121
123
  ),
122
124
  /* @__PURE__ */ jsx(Divider, {})
@@ -1 +1 @@
1
- {"version":3,"file":"SideMenu.js","sources":["../../src/appShell/SideMenu.jsx"],"sourcesContent":["// Lo de adentro del menú lateral (el fijo de escritorio y el cajón del teléfono): arriba el logo y el nombre de la app\n// (solo en el cajón del teléfono: en escritorio van en la barra), en medio las secciones del menú (composeMenu, con el\n// título de cada sección) y abajo lo que el proyecto quiera (sidebarFooter: una tarjeta de ayuda, el plan contratado…).\n//\n// Es una columna de toda la altura: el menú crece y se desliza solo, y el pie se queda abajo.\n// Encogido (solo íconos): el título de cada sección se vuelve una rayita del mismo alto (los huecos entre secciones se\n// quedan parejos y nada brinca al abrirlo), el pie se quita (escondido seguía ocupando su lugar y sacaba la barra de\n// desplazamiento). La barra de desplazamiento no se pinta nunca: se comía el ancho de los nombres (la rueda sigue).\n// Con el vidrio del tema, los títulos de sección van como se escriben («Operación») y no en mayúsculas: la guía de Liquid\n// Glass de Apple los dejó de poner en mayúsculas porque se leen mejor.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useRef, useState} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport Box from '@mui/material/Box';\nimport ButtonBase from '@mui/material/ButtonBase';\nimport Divider from '@mui/material/Divider';\nimport IconButton from '@mui/material/IconButton';\nimport List from '@mui/material/List';\nimport ListSubheader from '@mui/material/ListSubheader';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport {useTheme} from '@mui/material/styles';\nimport KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded';\nimport KeyboardArrowUpRoundedIcon from '@mui/icons-material/KeyboardArrowUpRounded';\n\nimport {menuItemLabelOf, menuSectionsOf} from '@/appShell/menuItemText.js';\nimport {APP_BAR_HEIGHT_PIXELS} from '@/appShell/shellSizes.js';\nimport SideMenuEntry from '@/appShell/SideMenuEntry.jsx';\nimport {glassStylesOf, isGlassTheme} from '@/theme/glassSurface.js';\n\n// lo alto del desvanecido en la orilla del menú que tiene más (px)\nconst SCROLL_FADE_PIXELS = 28;\n// el estilo minimalista del tema (THEME_LOOK.minimal; se repite el texto para no importar el tema desde el menú)\nconst MINIMAL_LOOK = 'minimal';\n// la flecha: a cuánto de la orilla va y cuánto se repite al deslizar un tramo (px)\nconst SCROLL_ARROW_INSET_PIXELS = 4;\nconst SCROLL_STEP_OVERLAP_PIXELS = 44;\n\n/** La flecha de una orilla del menú: un botoncito de vidrio centrado que lo desliza un tramo. */\nfunction ScrollArrow({label, onClick, sx, children}) {\n return (\n <Tooltip title={label} placement=\"right\">\n <IconButton size=\"small\" aria-label={label} onClick={onClick}\n sx={[(theme) => ({\n position: 'absolute', left: '50%', transform: 'translateX(-50%)', zIndex: 1, width: 32, height: 24, borderRadius: '999px',\n ...(isGlassTheme(theme) ? glassStylesOf(theme, {isControl: true}) : {bgcolor: 'background.paper', boxShadow: 2}),\n }), sx]}>\n {children}\n </IconButton>\n </Tooltip>\n );\n}\n\n// el logo de 28 px queda centrado en el menú encogido de 64 px: (64 - 28) / 2\nconst BRAND_LEFT_PADDING = '18px';\n// la letra del título de sección (rem): en mayúsculas, más chica; con vidrio, como se escribe, un poco más grande. Con\n// alto de renglón 2, la mitad del renglón mide lo mismo que la letra: ahí va la rayita del encogido\nconst SECTION_TITLE_FONT_REM = Object.freeze({solid: 0.6875, glass: 0.75});\n\n/**\n * @param {object} props\n * @param {import('react').ReactNode} [props.logo] el logo chico (28 px), el que se queda con el menú encogido\n * @param {string} props.brandName el nombre de la app\n * @param {string} [props.brandCaption] el renglón chico debajo del nombre («Panel de administración»)\n * @param {string} props.homePath\n * @param {object[]} props.menuItems lo que regresa composeMenu\n * @param {object|null} props.activeItem\n * @param {boolean} props.collapsed\n * @param {import('react').ReactNode} [props.footer]\n * @param {boolean} [props.showBrand] el logo y el nombre arriba; por omisión sí (en escritorio van en la barra)\n * @param {() => void} [props.onNavigate]\n * @param {Record<string, number>} [props.menuBadges] los conteos del servidor por badgeKey\n * @param {import('react').ReactNode} [props.railTop] arriba del menú encogido que hace de barra: la ☰ y el logo\n * @param {import('react').ReactNode} [props.railBottom] abajo de ese menú: los ajustes y la cuenta\n */\nexport default function SideMenu({\n logo, brandName, brandCaption, homePath, menuItems, activeItem, collapsed, footer, showBrand = true, onNavigate, railTop, railBottom, menuBadges,\n}) {\n const {t} = useToolkitTranslation('layout');\n const {Link: RouterLink} = useToolkitNavigation();\n const theme = useTheme();\n // con vidrio y con el minimalista (los dos siguen a Apple) los títulos de sección van como se escriben, no en mayúsculas\n const isAppleStyle = isGlassTheme(theme) || theme.toolkit?.look === MINIMAL_LOOK;\n const sectionTitleFontRem = isAppleStyle ? SECTION_TITLE_FONT_REM.glass : SECTION_TITLE_FONT_REM.solid;\n const fadeWhenCollapsed = (theme) => ({opacity: collapsed ? 0 : 1, transition: theme.transitions.create('opacity')});\n // el menú no lleva barra de desplazamiento (ver abajo): cuando no cabe, se desvanece en la orilla de donde hay más, para\n // que se entienda que se desliza (lo reportaron: «no se entiende que scrollea»)\n const listRef = useRef(null);\n const [hiddenEdges, setHiddenEdges] = useState({top: false, bottom: false});\n useEffect(() => {\n const list = listRef.current;\n if (!list) {\n return undefined;\n }\n const measure = () => setHiddenEdges({\n top: list.scrollTop > 1,\n bottom: list.scrollTop + list.clientHeight < list.scrollHeight - 1,\n });\n measure();\n list.addEventListener('scroll', measure, {passive: true});\n const resizeObserver = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(measure);\n resizeObserver?.observe(list);\n return () => {\n list.removeEventListener('scroll', measure);\n resizeObserver?.disconnect();\n };\n }, [menuItems, collapsed]);\n // la flecha desliza el menú lo que mide su parte visible, menos un renglón para no perder de vista dónde se iba\n const scrollMenu = (direction) => listRef.current?.scrollBy({top: direction * (listRef.current.clientHeight - SCROLL_STEP_OVERLAP_PIXELS), behavior: 'smooth'});\n const fadeMask = `linear-gradient(to bottom, ${hiddenEdges.top ? 'transparent' : 'black'}, black ${SCROLL_FADE_PIXELS}px, black calc(100% - ${SCROLL_FADE_PIXELS}px), ${hiddenEdges.bottom ? 'transparent' : 'black'})`;\n\n return (\n // OJO: flex y no height 100%: dentro de la hoja de «Más» (que solo tiene alto máximo) un 100 % no se resuelve, el menú\n // crecía sin límite y la hoja no se deslizaba (lo reportaron con el teléfono acostado)\n <Box sx={{display: 'flex', flexDirection: 'column', flex: '1 1 auto', minHeight: 0}}>\n {showBrand && (\n <>\n <ButtonBase component={RouterLink} to={homePath} onClick={onNavigate} aria-label={brandName}\n sx={{display: 'flex', justifyContent: 'flex-start', alignItems: 'center', gap: 1.25, minHeight: APP_BAR_HEIGHT_PIXELS,\n pl: BRAND_LEFT_PADDING, pr: 2, flexShrink: 0}}>\n <Box sx={{width: 28, height: 28, flex: '0 0 auto', display: 'flex', alignItems: 'center', justifyContent: 'center'}}>{logo}</Box>\n <Box sx={(theme) => ({minWidth: 0, textAlign: 'left', ...fadeWhenCollapsed(theme)})}>\n <Typography variant=\"subtitle2\" component=\"span\" noWrap sx={{display: 'block', fontWeight: 600, lineHeight: 1.3}}>{brandName}</Typography>\n {brandCaption && <Typography variant=\"caption\" component=\"span\" color=\"text.secondary\" noWrap sx={{display: 'block', lineHeight: 1.3}}>{brandCaption}</Typography>}\n </Box>\n </ButtonBase>\n <Divider />\n </>\n )}\n\n {railTop && <Box sx={{display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1, pt: 1.25, pb: 0.5, flexShrink: 0}}>{railTop}</Box>}\n\n {/* la lista y sus flechas: cuando no cabe, una flecha en la orilla de donde hay más (como los menús largos de la Mac)\n que la desliza un tramo; el desvanecido la refuerza. Sin barra de desplazamiento no se entendía que había más */}\n <Box sx={{position: 'relative', flexGrow: 1, minHeight: 0, display: 'flex', flexDirection: 'column'}}>\n <List ref={listRef} component=\"nav\" aria-label={t('drawer.ariaLabel')} dense disablePadding\n sx={{\n px: 1, pt: 1, pb: 2, flexGrow: 1, minHeight: 0, height: '100%', overflowY: 'auto', overflowX: 'hidden',\n // sin barra de desplazamiento, abierto o encogido (se desliza igual con la rueda, el trackpad o el\n // teclado, como en la Mac, donde la barra solo sale al deslizar): a la vista se veía una raya junto a\n // los nombres, y aunque fuera transparente se comía su ancho y cortaba «Alta con react-hook-form»\n scrollbarWidth: 'none',\n maskImage: fadeMask,\n WebkitMaskImage: fadeMask,\n '&::-webkit-scrollbar': {display: 'none'},\n }}>\n {menuSectionsOf(menuItems, t).map((section, sectionIndex) => (\n <Box component=\"li\" key={section.title ?? `section-${sectionIndex}`} sx={{listStyle: 'none'}}>\n <List dense disablePadding\n subheader={section.title ? (\n <ListSubheader disableSticky component=\"div\"\n sx={(theme) => ({position: 'relative', bgcolor: 'transparent', px: 1.5, pt: 1.5, lineHeight: 2, fontSize: `${sectionTitleFontRem}rem`,\n fontWeight: 600, letterSpacing: isAppleStyle ? 0 : '0.08em', textTransform: isAppleStyle ? 'none' : 'uppercase',\n color: 'text.secondary', whiteSpace: 'nowrap',\n // encogido: en lugar del título, una rayita a media altura del renglón\n '&::after': {\n content: '\"\"', position: 'absolute', left: '25%', right: '25%', height: '1px',\n top: `calc(${theme.spacing(1.5)} + ${sectionTitleFontRem}rem)`, bgcolor: 'divider',\n opacity: collapsed ? 1 : 0, transition: theme.transitions.create('opacity'),\n }})}>\n <Box component=\"span\" sx={fadeWhenCollapsed}>{section.title}</Box>\n </ListSubheader>\n ) : null}>\n {section.menuItems.map((menuItem) => (\n <SideMenuEntry key={menuItem.to ?? menuItemLabelOf(menuItem, t)} menuItem={menuItem} activeItem={activeItem} collapsed={collapsed}\n onNavigate={onNavigate} menuBadges={menuBadges} />\n ))}\n </List>\n </Box>\n ))}\n {/* la tarjeta de abajo va al final de lo que se desliza, no fija: en una pantalla baja (el teléfono acostado) se\n comía el menú y solo se veía «Inicio» (lo reportaron) */}\n {footer && (\n <Box component=\"li\" sx={{listStyle: 'none', pt: 2, px: 0.5, display: collapsed ? 'none' : 'block'}}>{footer}</Box>\n )}\n </List>\n {hiddenEdges.top && (\n <ScrollArrow label={t('drawer.scrollUp')} onClick={() => scrollMenu(-1)} sx={{top: SCROLL_ARROW_INSET_PIXELS}}>\n <KeyboardArrowUpRoundedIcon fontSize=\"small\" />\n </ScrollArrow>\n )}\n {hiddenEdges.bottom && (\n <ScrollArrow label={t('drawer.scrollDown')} onClick={() => scrollMenu(1)} sx={{bottom: SCROLL_ARROW_INSET_PIXELS}}>\n <KeyboardArrowDownRoundedIcon fontSize=\"small\" />\n </ScrollArrow>\n )}\n </Box>\n\n {railBottom && <Box sx={{display: 'flex', justifyContent: 'center', pt: 1, pb: 1.25, flexShrink: 0, borderTop: 1, borderColor: 'divider'}}>{railBottom}</Box>}\n </Box>\n );\n}\n"],"names":["theme"],"mappings":"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"}
1
+ {"version":3,"file":"SideMenu.js","sources":["../../src/appShell/SideMenu.jsx"],"sourcesContent":["// Lo de adentro del menú lateral (el fijo de escritorio y el cajón del teléfono): arriba el logo y el nombre de la app\n// (solo en el cajón del teléfono: en escritorio van en la barra), en medio las secciones del menú (composeMenu, con el\n// título de cada sección) y abajo lo que el proyecto quiera (sidebarFooter: una tarjeta de ayuda, el plan contratado…).\n//\n// Es una columna de toda la altura: el menú crece y se desliza solo, y el pie se queda abajo.\n// Encogido (solo íconos): el título de cada sección se vuelve una rayita del mismo alto (los huecos entre secciones se\n// quedan parejos y nada brinca al abrirlo), el pie se quita (escondido seguía ocupando su lugar y sacaba la barra de\n// desplazamiento). La barra de desplazamiento no se pinta nunca: se comía el ancho de los nombres (la rueda sigue).\n// Con el vidrio del tema, los títulos de sección van como se escriben («Operación») y no en mayúsculas: la guía de Liquid\n// Glass de Apple los dejó de poner en mayúsculas porque se leen mejor.\n//\n// Genérico del paquete: function (regla de la casa).\nimport {useEffect, useRef, useState} from 'react';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport Box from '@mui/material/Box';\nimport ButtonBase from '@mui/material/ButtonBase';\nimport Divider from '@mui/material/Divider';\nimport IconButton from '@mui/material/IconButton';\nimport List from '@mui/material/List';\nimport ListSubheader from '@mui/material/ListSubheader';\nimport Tooltip from '@mui/material/Tooltip';\nimport Typography from '@mui/material/Typography';\nimport {useTheme} from '@mui/material/styles';\nimport KeyboardArrowDownRoundedIcon from '@mui/icons-material/KeyboardArrowDownRounded';\nimport KeyboardArrowUpRoundedIcon from '@mui/icons-material/KeyboardArrowUpRounded';\n\nimport {menuItemLabelOf, menuSectionsOf} from '@/appShell/menuItemText.js';\nimport Wordmark from '@/appShell/Wordmark.jsx';\nimport {APP_BAR_HEIGHT_PIXELS} from '@/appShell/shellSizes.js';\nimport SideMenuEntry from '@/appShell/SideMenuEntry.jsx';\nimport {glassStylesOf, isGlassTheme} from '@/theme/glassSurface.js';\n\n// lo alto del desvanecido en la orilla del menú que tiene más (px)\nconst SCROLL_FADE_PIXELS = 28;\n// el estilo minimalista del tema (THEME_LOOK.minimal; se repite el texto para no importar el tema desde el menú)\nconst MINIMAL_LOOK = 'minimal';\n// la flecha: a cuánto de la orilla va y cuánto se repite al deslizar un tramo (px)\nconst SCROLL_ARROW_INSET_PIXELS = 4;\nconst SCROLL_STEP_OVERLAP_PIXELS = 44;\n\n/** La flecha de una orilla del menú: un botoncito de vidrio centrado que lo desliza un tramo. */\nfunction ScrollArrow({label, onClick, sx, children}) {\n return (\n <Tooltip title={label} placement=\"right\">\n <IconButton size=\"small\" aria-label={label} onClick={onClick}\n sx={[(theme) => ({\n position: 'absolute', left: '50%', transform: 'translateX(-50%)', zIndex: 1, width: 32, height: 24, borderRadius: '999px',\n ...(isGlassTheme(theme) ? glassStylesOf(theme, {isControl: true}) : {bgcolor: 'background.paper', boxShadow: 2}),\n }), sx]}>\n {children}\n </IconButton>\n </Tooltip>\n );\n}\n\n// el logo de 28 px queda centrado en el menú encogido de 64 px: (64 - 28) / 2\nconst BRAND_LEFT_PADDING = '18px';\n// la letra del título de sección (rem): en mayúsculas, más chica; con vidrio, como se escribe, un poco más grande. Con\n// alto de renglón 2, la mitad del renglón mide lo mismo que la letra: ahí va la rayita del encogido\nconst SECTION_TITLE_FONT_REM = Object.freeze({solid: 0.6875, glass: 0.75});\n\n/**\n * @param {object} props\n * @param {import('react').ReactNode} [props.logo] el logo chico (28 px), el que se queda con el menú encogido\n * @param {import('react').ReactNode} [props.wordmark] el logo ancho (en lugar del logo y el nombre arriba)\n * @param {string} props.brandName el nombre de la app\n * @param {string} [props.brandCaption] el renglón chico debajo del nombre («Panel de administración»)\n * @param {string} props.homePath\n * @param {object[]} props.menuItems lo que regresa composeMenu\n * @param {object|null} props.activeItem\n * @param {boolean} props.collapsed\n * @param {import('react').ReactNode} [props.footer]\n * @param {boolean} [props.showBrand] el logo y el nombre arriba; por omisión sí (en escritorio van en la barra)\n * @param {() => void} [props.onNavigate]\n * @param {Record<string, number>} [props.menuBadges] los conteos del servidor por badgeKey\n * @param {import('react').ReactNode} [props.railTop] arriba del menú encogido que hace de barra: la ☰ y el logo\n * @param {import('react').ReactNode} [props.railBottom] abajo de ese menú: los ajustes y la cuenta\n */\nexport default function SideMenu({\n logo, wordmark, brandName, brandCaption, homePath, menuItems, activeItem, collapsed, footer, showBrand = true, onNavigate, railTop, railBottom, menuBadges,\n}) {\n const {t} = useToolkitTranslation('layout');\n const {Link: RouterLink} = useToolkitNavigation();\n const theme = useTheme();\n // con vidrio y con el minimalista (los dos siguen a Apple) los títulos de sección van como se escriben, no en mayúsculas\n const isAppleStyle = isGlassTheme(theme) || theme.toolkit?.look === MINIMAL_LOOK;\n const sectionTitleFontRem = isAppleStyle ? SECTION_TITLE_FONT_REM.glass : SECTION_TITLE_FONT_REM.solid;\n const fadeWhenCollapsed = (theme) => ({opacity: collapsed ? 0 : 1, transition: theme.transitions.create('opacity')});\n // el menú no lleva barra de desplazamiento (ver abajo): cuando no cabe, se desvanece en la orilla de donde hay más, para\n // que se entienda que se desliza (lo reportaron: «no se entiende que scrollea»)\n const listRef = useRef(null);\n const [hiddenEdges, setHiddenEdges] = useState({top: false, bottom: false});\n useEffect(() => {\n const list = listRef.current;\n if (!list) {\n return undefined;\n }\n const measure = () => setHiddenEdges({\n top: list.scrollTop > 1,\n bottom: list.scrollTop + list.clientHeight < list.scrollHeight - 1,\n });\n measure();\n list.addEventListener('scroll', measure, {passive: true});\n const resizeObserver = typeof ResizeObserver === 'undefined' ? null : new ResizeObserver(measure);\n resizeObserver?.observe(list);\n return () => {\n list.removeEventListener('scroll', measure);\n resizeObserver?.disconnect();\n };\n }, [menuItems, collapsed]);\n // la flecha desliza el menú lo que mide su parte visible, menos un renglón para no perder de vista dónde se iba\n const scrollMenu = (direction) => listRef.current?.scrollBy({top: direction * (listRef.current.clientHeight - SCROLL_STEP_OVERLAP_PIXELS), behavior: 'smooth'});\n const fadeMask = `linear-gradient(to bottom, ${hiddenEdges.top ? 'transparent' : 'black'}, black ${SCROLL_FADE_PIXELS}px, black calc(100% - ${SCROLL_FADE_PIXELS}px), ${hiddenEdges.bottom ? 'transparent' : 'black'})`;\n\n return (\n // OJO: flex y no height 100%: dentro de la hoja de «Más» (que solo tiene alto máximo) un 100 % no se resuelve, el menú\n // crecía sin límite y la hoja no se deslizaba (lo reportaron con el teléfono acostado)\n <Box sx={{display: 'flex', flexDirection: 'column', flex: '1 1 auto', minHeight: 0}}>\n {showBrand && (\n <>\n <ButtonBase component={RouterLink} to={homePath} onClick={onNavigate} aria-label={brandName}\n sx={{display: 'flex', justifyContent: 'flex-start', alignItems: 'center', gap: 1.25, minHeight: APP_BAR_HEIGHT_PIXELS,\n pl: BRAND_LEFT_PADDING, pr: 2, flexShrink: 0}}>\n {wordmark ? <Wordmark>{wordmark}</Wordmark> : (\n <>\n <Box sx={{width: 28, height: 28, flex: '0 0 auto', display: 'flex', alignItems: 'center', justifyContent: 'center'}}>{logo}</Box>\n <Box sx={(theme) => ({minWidth: 0, textAlign: 'left', ...fadeWhenCollapsed(theme)})}>\n <Typography variant=\"subtitle2\" component=\"span\" noWrap sx={{display: 'block', fontWeight: 600, lineHeight: 1.3}}>{brandName}</Typography>\n {brandCaption && <Typography variant=\"caption\" component=\"span\" color=\"text.secondary\" noWrap sx={{display: 'block', lineHeight: 1.3}}>{brandCaption}</Typography>}\n </Box>\n </>\n )}\n </ButtonBase>\n <Divider />\n </>\n )}\n\n {railTop && <Box sx={{display: 'flex', flexDirection: 'column', alignItems: 'center', gap: 1, pt: 1.25, pb: 0.5, flexShrink: 0}}>{railTop}</Box>}\n\n {/* la lista y sus flechas: cuando no cabe, una flecha en la orilla de donde hay más (como los menús largos de la Mac)\n que la desliza un tramo; el desvanecido la refuerza. Sin barra de desplazamiento no se entendía que había más */}\n <Box sx={{position: 'relative', flexGrow: 1, minHeight: 0, display: 'flex', flexDirection: 'column'}}>\n <List ref={listRef} component=\"nav\" aria-label={t('drawer.ariaLabel')} dense disablePadding\n sx={{\n px: 1, pt: 1, pb: 2, flexGrow: 1, minHeight: 0, height: '100%', overflowY: 'auto', overflowX: 'hidden',\n // sin barra de desplazamiento, abierto o encogido (se desliza igual con la rueda, el trackpad o el\n // teclado, como en la Mac, donde la barra solo sale al deslizar): a la vista se veía una raya junto a\n // los nombres, y aunque fuera transparente se comía su ancho y cortaba «Alta con react-hook-form»\n scrollbarWidth: 'none',\n maskImage: fadeMask,\n WebkitMaskImage: fadeMask,\n '&::-webkit-scrollbar': {display: 'none'},\n }}>\n {menuSectionsOf(menuItems, t).map((section, sectionIndex) => (\n <Box component=\"li\" key={section.title ?? `section-${sectionIndex}`} sx={{listStyle: 'none'}}>\n <List dense disablePadding\n subheader={section.title ? (\n <ListSubheader disableSticky component=\"div\"\n sx={(theme) => ({position: 'relative', bgcolor: 'transparent', px: 1.5, pt: 1.5, lineHeight: 2, fontSize: `${sectionTitleFontRem}rem`,\n fontWeight: 600, letterSpacing: isAppleStyle ? 0 : '0.08em', textTransform: isAppleStyle ? 'none' : 'uppercase',\n color: 'text.secondary', whiteSpace: 'nowrap',\n // encogido: en lugar del título, una rayita a media altura del renglón\n '&::after': {\n content: '\"\"', position: 'absolute', left: '25%', right: '25%', height: '1px',\n top: `calc(${theme.spacing(1.5)} + ${sectionTitleFontRem}rem)`, bgcolor: 'divider',\n opacity: collapsed ? 1 : 0, transition: theme.transitions.create('opacity'),\n }})}>\n <Box component=\"span\" sx={fadeWhenCollapsed}>{section.title}</Box>\n </ListSubheader>\n ) : null}>\n {section.menuItems.map((menuItem) => (\n <SideMenuEntry key={menuItem.to ?? menuItemLabelOf(menuItem, t)} menuItem={menuItem} activeItem={activeItem} collapsed={collapsed}\n onNavigate={onNavigate} menuBadges={menuBadges} />\n ))}\n </List>\n </Box>\n ))}\n {/* la tarjeta de abajo va al final de lo que se desliza, no fija: en una pantalla baja (el teléfono acostado) se\n comía el menú y solo se veía «Inicio» (lo reportaron) */}\n {footer && (\n <Box component=\"li\" sx={{listStyle: 'none', pt: 2, px: 0.5, display: collapsed ? 'none' : 'block'}}>{footer}</Box>\n )}\n </List>\n {hiddenEdges.top && (\n <ScrollArrow label={t('drawer.scrollUp')} onClick={() => scrollMenu(-1)} sx={{top: SCROLL_ARROW_INSET_PIXELS}}>\n <KeyboardArrowUpRoundedIcon fontSize=\"small\" />\n </ScrollArrow>\n )}\n {hiddenEdges.bottom && (\n <ScrollArrow label={t('drawer.scrollDown')} onClick={() => scrollMenu(1)} sx={{bottom: SCROLL_ARROW_INSET_PIXELS}}>\n <KeyboardArrowDownRoundedIcon fontSize=\"small\" />\n </ScrollArrow>\n )}\n </Box>\n\n {railBottom && <Box sx={{display: 'flex', justifyContent: 'center', pt: 1, pb: 1.25, flexShrink: 0, borderTop: 1, borderColor: 'divider'}}>{railBottom}</Box>}\n </Box>\n );\n}\n"],"names":["theme"],"mappings":"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"}
@@ -15,7 +15,7 @@ import Tooltip from "@mui/material/Tooltip";
15
15
  import { useTheme } from "@mui/material/styles";
16
16
  import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
17
17
  import ChevronRightIcon from "@mui/icons-material/ChevronRight";
18
- import { menuItemLabelOf } from "./menuItemText.js";
18
+ import { menuItemLabelOf, menuLinkPropsOf } from "./menuItemText.js";
19
19
  import SubmenuActionsMenu, { SUBMENU_PLACEMENT } from "./SubmenuActionsMenu.js";
20
20
  import { menuBadgeOf } from "../navigation/appMenu.js";
21
21
  import { isGlassTheme, accentTextStylesOf, glassDropOf } from "../theme/glassSurface.js";
@@ -59,7 +59,7 @@ function SideMenuEntry({ menuItem, activeItem, collapsed, onNavigate, menuBadges
59
59
  setOpenedByPerson(!isOpen);
60
60
  };
61
61
  const closeFlyout = () => setFlyoutAnchor(null);
62
- const linkProps = hasChildren ? { onClick: toggleChildren, ...collapsed && { "aria-haspopup": "menu" } } : { component: RouterLink, to: menuItem.to, onClick: onNavigate };
62
+ const linkProps = hasChildren ? { onClick: toggleChildren, ...collapsed && { "aria-haspopup": "menu" } } : { ...menuLinkPropsOf(menuItem, RouterLink), onClick: onNavigate };
63
63
  const fadeWhenCollapsed = (theme2) => ({ opacity: collapsed ? 0 : 1, transition: theme2.transitions.create("opacity") });
64
64
  const isChild = depth > 0;
65
65
  const leftPadding = !isChild || Icon ? theme.spacing(ROW_LEFT_PADDING + depth * INDENT_PER_LEVEL) : `calc(${theme.spacing(ROW_LEFT_PADDING + ICON_GAP + (depth - 1) * INDENT_PER_LEVEL)} + ${ICON_SIZE})`;
@@ -130,7 +130,7 @@ function SideMenuEntry({ menuItem, activeItem, collapsed, onNavigate, menuBadges
130
130
  menuBadges,
131
131
  depth: depth + 1
132
132
  },
133
- child.to ?? menuItemLabelOf(child, t)
133
+ child.to ?? child.href ?? menuItemLabelOf(child, t)
134
134
  )) }) })
135
135
  ] });
136
136
  }
@@ -1 +1 @@
1
- {"version":3,"file":"SideMenuEntry.js","sources":["../../src/appShell/SideMenuEntry.jsx"],"sourcesContent":["// Un renglón del menú lateral: ícono, nombre y (si trae) su numerito; si tiene submenú, se abre y se cierra en su lugar.\n// Con el menú encogido, el submenú sale como un menú flotante a la derecha del ícono (el de acciones de Apple: ícono y\n// nombre, la pantalla abierta marcada) en lugar de abrir todo el menú. Muestra un nivel: las hijas directas.\n//\n// Reglas que vienen del menú lateral de walk-ot, ya pagadas:\n// - La estructura es FIJA: al encoger el menú, el nombre y la flecha NO se desmontan, solo se vuelven transparentes. Si\n// se desmontaran, React los quita en el mismo cuadro en que el menú anima su ancho y el ojo lo lee como un salto.\n// - Encogido, el nombre sale en un tooltip a la derecha.\n// - Los tamaños van en rem, no en px: así crecen con el tamaño de letra que elige la persona (FontSizeToggle).\n// - El renglón es una liga de verdad (clic con la rueda, «abrir en otra pestaña»), no un onClick que navega.\n// - Una hija del submenú con ícono se mete un escalón; una sin ícono no deja hueco vacío: su nombre queda justo debajo\n// del de su madre (con el hueco, el nombre se cortaba: «Alta con react-hook-fo…»).\n//\n// Con el vidrio del tema, lo que pide la guía de Liquid Glass de Apple:\n// - lo elegido va en la «gota» (la misma de las pestañas), no en un fondo plano;\n// - encogido, el menú es un panel con las esquinas del expandido (20) y cada renglón un botón de 44 px (el mínimo para el\n// dedo) con esquina concéntrica: el radio del panel (20) menos el margen entre botón y orilla (10) da 10. Antes era una\n// cápsula con botones redondos y se veía demasiado redondo (lo reportaron).\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 Badge from '@mui/material/Badge';\nimport Box from '@mui/material/Box';\nimport Chip from '@mui/material/Chip';\nimport Collapse from '@mui/material/Collapse';\nimport List from '@mui/material/List';\nimport ListItemButton from '@mui/material/ListItemButton';\nimport ListItemIcon from '@mui/material/ListItemIcon';\nimport ListItemText from '@mui/material/ListItemText';\nimport Tooltip from '@mui/material/Tooltip';\nimport {useTheme} from '@mui/material/styles';\nimport ExpandMoreIcon from '@mui/icons-material/ExpandMore';\nimport ChevronRightIcon from '@mui/icons-material/ChevronRight';\n\nimport {menuItemLabelOf} from '@/appShell/menuItemText.js';\nimport SubmenuActionsMenu, {SUBMENU_PLACEMENT} from '@/appShell/SubmenuActionsMenu.jsx';\nimport {menuBadgeOf} from '@/navigation/appMenu.js';\nimport {accentTextStylesOf, glassDropOf, isGlassTheme} from '@/theme/glassSurface.js';\nimport {mergeStyles} from '@/theme/mergeComponents.js';\nimport {textSizeOf} from '@/theme/appleTracking.js';\n\n// las medidas del renglón, en unidades de spacing: los márgenes, el escalón de cada nivel de submenú y el espacio entre el\n// ícono y el nombre. Las hijas van más apretadas (ícono más chico, menos espacio, renglón más bajo): medido en Chrome, con\n// las medidas de las madres, «Alta con react-hook-form» en negritas pedía 160 px, tenía 154 y se cortaba.\nconst ROW_LEFT_PADDING = 1.5;\nconst ROW_RIGHT_PADDING = 1.5;\nconst CHILD_RIGHT_PADDING = 1;\nconst INDENT_PER_LEVEL = 1.5;\nconst ICON_GAP = 1.75;\nconst CHILD_ICON_GAP = 1;\nconst ICON_SIZE = '1.375rem';\nconst CHILD_ICON_SIZE = '1.25rem';\nconst ROW_MIN_HEIGHT_PIXELS = 40;\nconst CHILD_ROW_MIN_HEIGHT_PIXELS = 36;\n// encogido con vidrio: el botón de 44 px (lo mínimo para tocar con el dedo, WCAG 2.5.5) con su esquina concéntrica\nconst RAIL_BUTTON_PIXELS = 44;\n// el nombre del renglón y su numerito (px), con su espaciado de Apple (textSizeOf)\nconst MENU_TEXT_PIXELS = 13;\nconst MENU_BADGE_PIXELS = 11;\n// de ahí para arriba se lee «99+»\nconst BADGE_MAX = 99;\nconst RAIL_BUTTON_RADIUS_PIXELS = 10;\n\n/**\n * @param {object} props\n * @param {object} props.menuItem una entrada de composeMenu\n * @param {object|null} props.activeItem la entrada de la pantalla abierta\n * @param {boolean} props.collapsed el menú está encogido (solo íconos)\n * @param {() => void} [props.onNavigate] al elegir una pantalla (el cajón del teléfono se cierra)\n * @param {Record<string, number>} [props.menuBadges] los conteos del servidor por badgeKey (menuBadgeOf)\n * @param {number} [props.depth]\n */\nexport default function SideMenuEntry({menuItem, activeItem, collapsed, onNavigate, menuBadges, depth = 0}) {\n const {t} = useToolkitTranslation('layout');\n const {Link: RouterLink} = useToolkitNavigation();\n const theme = useTheme();\n const isGlass = isGlassTheme(theme);\n const isRailButton = isGlass && collapsed;\n const hasChildren = Array.isArray(menuItem.children) && menuItem.children.length > 0;\n const containsActive = hasChildren && menuItem.children.includes(activeItem);\n const isActive = menuItem === activeItem || containsActive;\n // abierto si la persona lo abrió, o solo si ahí está la pantalla abierta\n const [openedByPerson, setOpenedByPerson] = useState(null);\n // encogido: el botón del que cuelga el menú flotante del submenú (null = cerrado)\n const [flyoutAnchor, setFlyoutAnchor] = useState(null);\n const isOpen = openedByPerson ?? containsActive;\n const label = menuItemLabelOf(menuItem, t);\n // el badge: el conteo del servidor, la suma de las hijas o el fijo del menu.js; encogido va sobre el ícono\n const badge = menuBadgeOf(menuItem, menuBadges);\n const Icon = menuItem.Icon;\n\n const toggleChildren = (event) => {\n if (collapsed) {\n setFlyoutAnchor(event.currentTarget);\n return;\n }\n setOpenedByPerson(!isOpen);\n };\n const closeFlyout = () => setFlyoutAnchor(null);\n\n const linkProps = hasChildren\n ? {onClick: toggleChildren, ...(collapsed && {'aria-haspopup': 'menu'})}\n : {component: RouterLink, to: menuItem.to, onClick: onNavigate};\n const fadeWhenCollapsed = (theme) => ({opacity: collapsed ? 0 : 1, transition: theme.transitions.create('opacity')});\n\n const isChild = depth > 0;\n // una hija sin ícono empieza donde empieza el nombre de su madre: margen + ícono + espacio, más los escalones de en medio\n const leftPadding = !isChild || Icon\n ? theme.spacing(ROW_LEFT_PADDING + depth * INDENT_PER_LEVEL)\n : `calc(${theme.spacing(ROW_LEFT_PADDING + ICON_GAP + (depth - 1) * INDENT_PER_LEVEL)} + ${ICON_SIZE})`;\n // el renglón de la pantalla abierta va del color del tema, pero legible (accentTextStylesOf): el naranja tal cual sobre\n // blanco daba 2.78 : 1\n const activeText = accentTextStylesOf(theme);\n // OJO: se SUMAN con mergeStyles, no con {...a, ...b}: los dos traen su parte de modo oscuro y con la suma simple la segunda\n // borraba la primera (el elegido salía en oscuro con el fondo de claro, casi negro y con línea blanca; error mío)\n const selectedStyle = mergeStyles(isGlass ? glassDropOf(theme) : {bgcolor: 'action.selected'}, activeText);\n\n const entryButton = (\n <ListItemButton {...linkProps} selected={!hasChildren && isActive} aria-expanded={hasChildren ? (collapsed ? Boolean(flyoutAnchor) : isOpen) : undefined}\n sx={{\n borderRadius: isRailButton ? `${RAIL_BUTTON_RADIUS_PIXELS}px` : 1.5, pl: leftPadding, pr: isChild ? CHILD_RIGHT_PADDING : ROW_RIGHT_PADDING,\n minHeight: isRailButton ? RAIL_BUTTON_PIXELS : (isChild ? CHILD_ROW_MIN_HEIGHT_PIXELS : ROW_MIN_HEIGHT_PIXELS),\n transition: theme.transitions.create(['background-color', 'border-radius', 'min-height']),\n ...(isActive ? activeText : {color: 'text.primary'}),\n '&.Mui-selected, &.Mui-selected:hover': selectedStyle,\n }}>\n {(!isChild || Icon) && (\n <ListItemIcon sx={{minWidth: 0, mr: isChild ? CHILD_ICON_GAP : ICON_GAP, color: 'inherit'}}>\n <Badge invisible={!collapsed || !badge} color=\"primary\" max={BADGE_MAX} badgeContent={badge?.isCount ? badge.content : undefined}\n variant={badge?.isCount ? 'standard' : 'dot'} overlap=\"circular\">\n {Icon ? <Icon sx={{fontSize: isChild ? CHILD_ICON_SIZE : ICON_SIZE}} /> : <Box sx={{width: ICON_SIZE, height: ICON_SIZE}} />}\n </Badge>\n </ListItemIcon>\n )}\n <ListItemText primary={label} sx={fadeWhenCollapsed}\n slotProps={{primary: {noWrap: true, sx: {...textSizeOf(MENU_TEXT_PIXELS), fontWeight: isActive ? 600 : 400}}}} />\n {badge && (\n <Chip label={badge.isCount && badge.content > BADGE_MAX ? `${BADGE_MAX}+` : badge.content} size=\"small\" color=\"primary\" sx={(theme) => ({height: 20, ...textSizeOf(MENU_BADGE_PIXELS), ml: 1, ...fadeWhenCollapsed(theme)})} />\n )}\n {hasChildren && (\n <Box sx={(theme) => ({display: 'flex', alignItems: 'center', ...fadeWhenCollapsed(theme)})}>\n {isOpen ? <ExpandMoreIcon fontSize=\"small\" /> : <ChevronRightIcon fontSize=\"small\" />}\n </Box>\n )}\n </ListItemButton>\n );\n\n return (\n <>\n {collapsed ? <Tooltip title={label} placement=\"right\">{entryButton}</Tooltip> : entryButton}\n {hasChildren && collapsed && (\n <SubmenuActionsMenu anchorElement={flyoutAnchor} onClose={closeFlyout} menuItem={menuItem} activeItem={activeItem} menuBadges={menuBadges}\n onNavigate={onNavigate} placement={SUBMENU_PLACEMENT.side} />\n )}\n {hasChildren && (\n <Collapse in={isOpen && !collapsed} unmountOnExit>\n <List dense disablePadding>\n {menuItem.children.map((child) => (\n <SideMenuEntry key={child.to ?? menuItemLabelOf(child, t)} menuItem={child} activeItem={activeItem} collapsed={collapsed}\n onNavigate={onNavigate} menuBadges={menuBadges} depth={depth + 1} />\n ))}\n </List>\n </Collapse>\n )}\n </>\n );\n}\n"],"names":["theme"],"mappings":"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"}
1
+ {"version":3,"file":"SideMenuEntry.js","sources":["../../src/appShell/SideMenuEntry.jsx"],"sourcesContent":["// Un renglón del menú lateral: ícono, nombre y (si trae) su numerito; si tiene submenú, se abre y se cierra en su lugar.\n// Con el menú encogido, el submenú sale como un menú flotante a la derecha del ícono (el de acciones de Apple: ícono y\n// nombre, la pantalla abierta marcada) en lugar de abrir todo el menú. Muestra un nivel: las hijas directas.\n//\n// Reglas que vienen del menú lateral de walk-ot, ya pagadas:\n// - La estructura es FIJA: al encoger el menú, el nombre y la flecha NO se desmontan, solo se vuelven transparentes. Si\n// se desmontaran, React los quita en el mismo cuadro en que el menú anima su ancho y el ojo lo lee como un salto.\n// - Encogido, el nombre sale en un tooltip a la derecha.\n// - Los tamaños van en rem, no en px: así crecen con el tamaño de letra que elige la persona (FontSizeToggle).\n// - El renglón es una liga de verdad (clic con la rueda, «abrir en otra pestaña»), no un onClick que navega.\n// - Una hija del submenú con ícono se mete un escalón; una sin ícono no deja hueco vacío: su nombre queda justo debajo\n// del de su madre (con el hueco, el nombre se cortaba: «Alta con react-hook-fo…»).\n//\n// Con el vidrio del tema, lo que pide la guía de Liquid Glass de Apple:\n// - lo elegido va en la «gota» (la misma de las pestañas), no en un fondo plano;\n// - encogido, el menú es un panel con las esquinas del expandido (20) y cada renglón un botón de 44 px (el mínimo para el\n// dedo) con esquina concéntrica: el radio del panel (20) menos el margen entre botón y orilla (10) da 10. Antes era una\n// cápsula con botones redondos y se veía demasiado redondo (lo reportaron).\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 Badge from '@mui/material/Badge';\nimport Box from '@mui/material/Box';\nimport Chip from '@mui/material/Chip';\nimport Collapse from '@mui/material/Collapse';\nimport List from '@mui/material/List';\nimport ListItemButton from '@mui/material/ListItemButton';\nimport ListItemIcon from '@mui/material/ListItemIcon';\nimport ListItemText from '@mui/material/ListItemText';\nimport Tooltip from '@mui/material/Tooltip';\nimport {useTheme} from '@mui/material/styles';\nimport ExpandMoreIcon from '@mui/icons-material/ExpandMore';\nimport ChevronRightIcon from '@mui/icons-material/ChevronRight';\n\nimport {menuItemLabelOf, menuLinkPropsOf} from '@/appShell/menuItemText.js';\nimport SubmenuActionsMenu, {SUBMENU_PLACEMENT} from '@/appShell/SubmenuActionsMenu.jsx';\nimport {menuBadgeOf} from '@/navigation/appMenu.js';\nimport {accentTextStylesOf, glassDropOf, isGlassTheme} from '@/theme/glassSurface.js';\nimport {mergeStyles} from '@/theme/mergeComponents.js';\nimport {textSizeOf} from '@/theme/appleTracking.js';\n\n// las medidas del renglón, en unidades de spacing: los márgenes, el escalón de cada nivel de submenú y el espacio entre el\n// ícono y el nombre. Las hijas van más apretadas (ícono más chico, menos espacio, renglón más bajo): medido en Chrome, con\n// las medidas de las madres, «Alta con react-hook-form» en negritas pedía 160 px, tenía 154 y se cortaba.\nconst ROW_LEFT_PADDING = 1.5;\nconst ROW_RIGHT_PADDING = 1.5;\nconst CHILD_RIGHT_PADDING = 1;\nconst INDENT_PER_LEVEL = 1.5;\nconst ICON_GAP = 1.75;\nconst CHILD_ICON_GAP = 1;\nconst ICON_SIZE = '1.375rem';\nconst CHILD_ICON_SIZE = '1.25rem';\nconst ROW_MIN_HEIGHT_PIXELS = 40;\nconst CHILD_ROW_MIN_HEIGHT_PIXELS = 36;\n// encogido con vidrio: el botón de 44 px (lo mínimo para tocar con el dedo, WCAG 2.5.5) con su esquina concéntrica\nconst RAIL_BUTTON_PIXELS = 44;\n// el nombre del renglón y su numerito (px), con su espaciado de Apple (textSizeOf)\nconst MENU_TEXT_PIXELS = 13;\nconst MENU_BADGE_PIXELS = 11;\n// de ahí para arriba se lee «99+»\nconst BADGE_MAX = 99;\nconst RAIL_BUTTON_RADIUS_PIXELS = 10;\n\n/**\n * @param {object} props\n * @param {object} props.menuItem una entrada de composeMenu\n * @param {object|null} props.activeItem la entrada de la pantalla abierta\n * @param {boolean} props.collapsed el menú está encogido (solo íconos)\n * @param {() => void} [props.onNavigate] al elegir una pantalla (el cajón del teléfono se cierra)\n * @param {Record<string, number>} [props.menuBadges] los conteos del servidor por badgeKey (menuBadgeOf)\n * @param {number} [props.depth]\n */\nexport default function SideMenuEntry({menuItem, activeItem, collapsed, onNavigate, menuBadges, depth = 0}) {\n const {t} = useToolkitTranslation('layout');\n const {Link: RouterLink} = useToolkitNavigation();\n const theme = useTheme();\n const isGlass = isGlassTheme(theme);\n const isRailButton = isGlass && collapsed;\n const hasChildren = Array.isArray(menuItem.children) && menuItem.children.length > 0;\n const containsActive = hasChildren && menuItem.children.includes(activeItem);\n const isActive = menuItem === activeItem || containsActive;\n // abierto si la persona lo abrió, o solo si ahí está la pantalla abierta\n const [openedByPerson, setOpenedByPerson] = useState(null);\n // encogido: el botón del que cuelga el menú flotante del submenú (null = cerrado)\n const [flyoutAnchor, setFlyoutAnchor] = useState(null);\n const isOpen = openedByPerson ?? containsActive;\n const label = menuItemLabelOf(menuItem, t);\n // el badge: el conteo del servidor, la suma de las hijas o el fijo del menu.js; encogido va sobre el ícono\n const badge = menuBadgeOf(menuItem, menuBadges);\n const Icon = menuItem.Icon;\n\n const toggleChildren = (event) => {\n if (collapsed) {\n setFlyoutAnchor(event.currentTarget);\n return;\n }\n setOpenedByPerson(!isOpen);\n };\n const closeFlyout = () => setFlyoutAnchor(null);\n\n const linkProps = hasChildren\n ? {onClick: toggleChildren, ...(collapsed && {'aria-haspopup': 'menu'})}\n : {...menuLinkPropsOf(menuItem, RouterLink), onClick: onNavigate};\n const fadeWhenCollapsed = (theme) => ({opacity: collapsed ? 0 : 1, transition: theme.transitions.create('opacity')});\n\n const isChild = depth > 0;\n // una hija sin ícono empieza donde empieza el nombre de su madre: margen + ícono + espacio, más los escalones de en medio\n const leftPadding = !isChild || Icon\n ? theme.spacing(ROW_LEFT_PADDING + depth * INDENT_PER_LEVEL)\n : `calc(${theme.spacing(ROW_LEFT_PADDING + ICON_GAP + (depth - 1) * INDENT_PER_LEVEL)} + ${ICON_SIZE})`;\n // el renglón de la pantalla abierta va del color del tema, pero legible (accentTextStylesOf): el naranja tal cual sobre\n // blanco daba 2.78 : 1\n const activeText = accentTextStylesOf(theme);\n // OJO: se SUMAN con mergeStyles, no con {...a, ...b}: los dos traen su parte de modo oscuro y con la suma simple la segunda\n // borraba la primera (el elegido salía en oscuro con el fondo de claro, casi negro y con línea blanca; error mío)\n const selectedStyle = mergeStyles(isGlass ? glassDropOf(theme) : {bgcolor: 'action.selected'}, activeText);\n\n const entryButton = (\n <ListItemButton {...linkProps} selected={!hasChildren && isActive} aria-expanded={hasChildren ? (collapsed ? Boolean(flyoutAnchor) : isOpen) : undefined}\n sx={{\n borderRadius: isRailButton ? `${RAIL_BUTTON_RADIUS_PIXELS}px` : 1.5, pl: leftPadding, pr: isChild ? CHILD_RIGHT_PADDING : ROW_RIGHT_PADDING,\n minHeight: isRailButton ? RAIL_BUTTON_PIXELS : (isChild ? CHILD_ROW_MIN_HEIGHT_PIXELS : ROW_MIN_HEIGHT_PIXELS),\n transition: theme.transitions.create(['background-color', 'border-radius', 'min-height']),\n ...(isActive ? activeText : {color: 'text.primary'}),\n '&.Mui-selected, &.Mui-selected:hover': selectedStyle,\n }}>\n {(!isChild || Icon) && (\n <ListItemIcon sx={{minWidth: 0, mr: isChild ? CHILD_ICON_GAP : ICON_GAP, color: 'inherit'}}>\n <Badge invisible={!collapsed || !badge} color=\"primary\" max={BADGE_MAX} badgeContent={badge?.isCount ? badge.content : undefined}\n variant={badge?.isCount ? 'standard' : 'dot'} overlap=\"circular\">\n {Icon ? <Icon sx={{fontSize: isChild ? CHILD_ICON_SIZE : ICON_SIZE}} /> : <Box sx={{width: ICON_SIZE, height: ICON_SIZE}} />}\n </Badge>\n </ListItemIcon>\n )}\n <ListItemText primary={label} sx={fadeWhenCollapsed}\n slotProps={{primary: {noWrap: true, sx: {...textSizeOf(MENU_TEXT_PIXELS), fontWeight: isActive ? 600 : 400}}}} />\n {badge && (\n <Chip label={badge.isCount && badge.content > BADGE_MAX ? `${BADGE_MAX}+` : badge.content} size=\"small\" color=\"primary\" sx={(theme) => ({height: 20, ...textSizeOf(MENU_BADGE_PIXELS), ml: 1, ...fadeWhenCollapsed(theme)})} />\n )}\n {hasChildren && (\n <Box sx={(theme) => ({display: 'flex', alignItems: 'center', ...fadeWhenCollapsed(theme)})}>\n {isOpen ? <ExpandMoreIcon fontSize=\"small\" /> : <ChevronRightIcon fontSize=\"small\" />}\n </Box>\n )}\n </ListItemButton>\n );\n\n return (\n <>\n {collapsed ? <Tooltip title={label} placement=\"right\">{entryButton}</Tooltip> : entryButton}\n {hasChildren && collapsed && (\n <SubmenuActionsMenu anchorElement={flyoutAnchor} onClose={closeFlyout} menuItem={menuItem} activeItem={activeItem} menuBadges={menuBadges}\n onNavigate={onNavigate} placement={SUBMENU_PLACEMENT.side} />\n )}\n {hasChildren && (\n <Collapse in={isOpen && !collapsed} unmountOnExit>\n <List dense disablePadding>\n {menuItem.children.map((child) => (\n <SideMenuEntry key={child.to ?? child.href ?? menuItemLabelOf(child, t)} menuItem={child} activeItem={activeItem} collapsed={collapsed}\n onNavigate={onNavigate} menuBadges={menuBadges} depth={depth + 1} />\n ))}\n </List>\n </Collapse>\n )}\n </>\n );\n}\n"],"names":["theme"],"mappings":"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"}
@@ -5,7 +5,7 @@ import ListItemIcon from "@mui/material/ListItemIcon";
5
5
  import ListItemText from "@mui/material/ListItemText";
6
6
  import Menu from "@mui/material/Menu";
7
7
  import MenuItem from "@mui/material/MenuItem";
8
- import { menuItemLabelOf } from "./menuItemText.js";
8
+ import { menuItemLabelOf, menuLinkPropsOf } from "./menuItemText.js";
9
9
  import { menuBadgeOf } from "../navigation/appMenu.js";
10
10
  import { textSizeOf } from "../theme/appleTracking.js";
11
11
  import { useToolkitNavigation } from "../toolkit/toolkitContext.js";
@@ -45,8 +45,7 @@ function SubmenuActionsMenu({ anchorElement, onClose, menuItem, activeItem, menu
45
45
  return /* @__PURE__ */ jsxs(
46
46
  MenuItem,
47
47
  {
48
- component: RouterLink,
49
- to: child.to,
48
+ ...menuLinkPropsOf(child, RouterLink),
50
49
  selected: child === activeItem,
51
50
  onClick: () => {
52
51
  onClose();
@@ -58,7 +57,7 @@ function SubmenuActionsMenu({ anchorElement, onClose, menuItem, activeItem, menu
58
57
  badge && /* @__PURE__ */ jsx(Chip, { label: badge.content, size: "small", color: "primary", sx: { height: 20, ml: 1.5, ...textSizeOf(BADGE_PIXELS) } })
59
58
  ]
60
59
  },
61
- child.to ?? menuItemLabelOf(child, t)
60
+ child.to ?? child.href ?? menuItemLabelOf(child, t)
62
61
  );
63
62
  })
64
63
  }
@@ -1 +1 @@
1
- {"version":3,"file":"SubmenuActionsMenu.js","sources":["../../src/appShell/SubmenuActionsMenu.jsx"],"sourcesContent":["// El submenú de una entrada como menú de acciones de Apple: un menú flotante con las hijas (ícono, nombre y su badge) y\n// la pantalla abierta marcada. Lo usan el menú lateral encogido (sale a la DERECHA del ícono) y la barra de pestañas del\n// teléfono (sale ARRIBA de la pestaña): un solo lugar para los dos.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Chip from '@mui/material/Chip';\nimport ListItemIcon from '@mui/material/ListItemIcon';\nimport ListItemText from '@mui/material/ListItemText';\nimport Menu from '@mui/material/Menu';\nimport MenuItem from '@mui/material/MenuItem';\n\nimport {menuItemLabelOf} from '@/appShell/menuItemText.js';\nimport {menuBadgeOf} from '@/navigation/appMenu.js';\nimport {textSizeOf} from '@/theme/appleTracking.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\n\n/** Hacia dónde sale el menú: a la derecha del botón (menú lateral) o arriba de él (pestañas del teléfono). */\nexport const SUBMENU_PLACEMENT = Object.freeze({side: 'side', above: 'above'});\n\n// lo que se separa el menú de su botón, lo ancho mínimo y el tamaño del badge (px)\nconst GAP_PIXELS = 12;\nconst MIN_WIDTH_PIXELS = 220;\nconst BADGE_PIXELS = 11;\n\nconst PLACEMENTS = Object.freeze({\n [SUBMENU_PLACEMENT.side]: {\n anchorOrigin: {vertical: 'top', horizontal: 'right'}, transformOrigin: {vertical: 'top', horizontal: 'left'}, gapSx: {ml: `${GAP_PIXELS}px`},\n },\n [SUBMENU_PLACEMENT.above]: {\n anchorOrigin: {vertical: 'top', horizontal: 'center'}, transformOrigin: {vertical: 'bottom', horizontal: 'center'}, gapSx: {mt: `-${GAP_PIXELS}px`},\n },\n});\n\n/**\n * @param {object} props\n * @param {Element|null} props.anchorElement el botón del que cuelga (null = cerrado)\n * @param {() => void} props.onClose\n * @param {object} props.menuItem la entrada con sus hijas (children)\n * @param {object|null} props.activeItem la entrada de la pantalla abierta\n * @param {Record<string, number>} [props.menuBadges] los conteos del servidor por badgeKey\n * @param {() => void} [props.onNavigate] al elegir una hija\n * @param {'side'|'above'} [props.placement] SUBMENU_PLACEMENT; por omisión a la derecha\n */\nexport default function SubmenuActionsMenu({anchorElement, onClose, menuItem, activeItem, menuBadges, onNavigate, placement = SUBMENU_PLACEMENT.side}) {\n const {t} = useToolkitTranslation('layout');\n const {Link: RouterLink} = useToolkitNavigation();\n const position = PLACEMENTS[placement] ?? PLACEMENTS[SUBMENU_PLACEMENT.side];\n\n return (\n <Menu anchorEl={anchorElement} open={Boolean(anchorElement)} onClose={onClose}\n anchorOrigin={position.anchorOrigin} transformOrigin={position.transformOrigin}\n slotProps={{paper: {sx: {minWidth: MIN_WIDTH_PIXELS, ...position.gapSx}}}}>\n {(menuItem.children ?? []).map((child) => {\n const ChildIcon = child.Icon;\n const badge = menuBadgeOf(child, menuBadges);\n return (\n <MenuItem key={child.to ?? menuItemLabelOf(child, t)} component={RouterLink} to={child.to} selected={child === activeItem}\n onClick={() => {\n onClose();\n onNavigate?.();\n }}>\n {ChildIcon && <ListItemIcon><ChildIcon fontSize=\"small\" /></ListItemIcon>}\n <ListItemText>{menuItemLabelOf(child, t)}</ListItemText>\n {badge && <Chip label={badge.content} size=\"small\" color=\"primary\" sx={{height: 20, ml: 1.5, ...textSizeOf(BADGE_PIXELS)}} />}\n </MenuItem>\n );\n })}\n </Menu>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAkBO,MAAM,oBAAoB,OAAO,OAAO,EAAC,MAAM,QAAQ,OAAO,SAAQ;AAG7E,MAAM,aAAa;AACnB,MAAM,mBAAmB;AACzB,MAAM,eAAe;AAErB,MAAM,aAAa,OAAO,OAAO;AAAA,EAC7B,CAAC,kBAAkB,IAAI,GAAG;AAAA,IACtB,cAAc,EAAC,UAAU,OAAO,YAAY,QAAA;AAAA,IAAU,iBAAiB,EAAC,UAAU,OAAO,YAAY,OAAA;AAAA,IAAS,OAAO,EAAC,IAAI,GAAG,UAAU,KAAA;AAAA,EAAI;AAAA,EAE/I,CAAC,kBAAkB,KAAK,GAAG;AAAA,IACvB,cAAc,EAAC,UAAU,OAAO,YAAY,SAAA;AAAA,IAAW,iBAAiB,EAAC,UAAU,UAAU,YAAY,SAAA;AAAA,IAAW,OAAO,EAAC,IAAI,IAAI,UAAU,KAAA;AAAA,EAAI;AAE1J,CAAC;AAYD,SAAwB,mBAAmB,EAAC,eAAe,SAAS,UAAU,YAAY,YAAY,YAAY,YAAY,kBAAkB,KAAA,GAAO;AACnJ,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ;AAC1C,QAAM,EAAC,MAAM,WAAA,IAAc,qBAAA;AAC3B,QAAM,WAAW,WAAW,SAAS,KAAK,WAAW,kBAAkB,IAAI;AAE3E,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAK,UAAU;AAAA,MAAe,MAAM,QAAQ,aAAa;AAAA,MAAG;AAAA,MACvD,cAAc,SAAS;AAAA,MAAc,iBAAiB,SAAS;AAAA,MAC/D,WAAW,EAAC,OAAO,EAAC,IAAI,EAAC,UAAU,kBAAkB,GAAG,SAAS,MAAA,IAAM;AAAA,MACvE,oBAAS,YAAY,CAAA,GAAI,IAAI,CAAC,UAAU;AACtC,cAAM,YAAY,MAAM;AACxB,cAAM,QAAQ,YAAY,OAAO,UAAU;AAC3C,eACI;AAAA,UAAC;AAAA,UAAA;AAAA,YAAqD,WAAW;AAAA,YAAY,IAAI,MAAM;AAAA,YAAI,UAAU,UAAU;AAAA,YACrG,SAAS,MAAM;AACX,sBAAA;AACA,2BAAA;AAAA,YACJ;AAAA,YACL,UAAA;AAAA,cAAA,iCAAc,cAAA,EAAa,UAAA,oBAAC,WAAA,EAAU,UAAS,SAAQ,GAAE;AAAA,cAC1D,oBAAC,cAAA,EAAc,UAAA,gBAAgB,OAAO,CAAC,GAAE;AAAA,cACxC,6BAAU,MAAA,EAAK,OAAO,MAAM,SAAS,MAAK,SAAQ,OAAM,WAAU,IAAI,EAAC,QAAQ,IAAI,IAAI,KAAK,GAAG,WAAW,YAAY,IAAC,CAAG;AAAA,YAAA;AAAA,UAAA;AAAA,UAPhH,MAAM,MAAM,gBAAgB,OAAO,CAAC;AAAA,QAAA;AAAA,MAU3D,CAAC;AAAA,IAAA;AAAA,EAAA;AAGb;"}
1
+ {"version":3,"file":"SubmenuActionsMenu.js","sources":["../../src/appShell/SubmenuActionsMenu.jsx"],"sourcesContent":["// El submenú de una entrada como menú de acciones de Apple: un menú flotante con las hijas (ícono, nombre y su badge) y\n// la pantalla abierta marcada. Lo usan el menú lateral encogido (sale a la DERECHA del ícono) y la barra de pestañas del\n// teléfono (sale ARRIBA de la pestaña): un solo lugar para los dos.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Chip from '@mui/material/Chip';\nimport ListItemIcon from '@mui/material/ListItemIcon';\nimport ListItemText from '@mui/material/ListItemText';\nimport Menu from '@mui/material/Menu';\nimport MenuItem from '@mui/material/MenuItem';\n\nimport {menuItemLabelOf, menuLinkPropsOf} from '@/appShell/menuItemText.js';\nimport {menuBadgeOf} from '@/navigation/appMenu.js';\nimport {textSizeOf} from '@/theme/appleTracking.js';\nimport {useToolkitNavigation} from '@/toolkit/toolkitContext.js';\nimport {useToolkitTranslation} from '@/toolkit/useToolkitTranslation.js';\n\n/** Hacia dónde sale el menú: a la derecha del botón (menú lateral) o arriba de él (pestañas del teléfono). */\nexport const SUBMENU_PLACEMENT = Object.freeze({side: 'side', above: 'above'});\n\n// lo que se separa el menú de su botón, lo ancho mínimo y el tamaño del badge (px)\nconst GAP_PIXELS = 12;\nconst MIN_WIDTH_PIXELS = 220;\nconst BADGE_PIXELS = 11;\n\nconst PLACEMENTS = Object.freeze({\n [SUBMENU_PLACEMENT.side]: {\n anchorOrigin: {vertical: 'top', horizontal: 'right'}, transformOrigin: {vertical: 'top', horizontal: 'left'}, gapSx: {ml: `${GAP_PIXELS}px`},\n },\n [SUBMENU_PLACEMENT.above]: {\n anchorOrigin: {vertical: 'top', horizontal: 'center'}, transformOrigin: {vertical: 'bottom', horizontal: 'center'}, gapSx: {mt: `-${GAP_PIXELS}px`},\n },\n});\n\n/**\n * @param {object} props\n * @param {Element|null} props.anchorElement el botón del que cuelga (null = cerrado)\n * @param {() => void} props.onClose\n * @param {object} props.menuItem la entrada con sus hijas (children)\n * @param {object|null} props.activeItem la entrada de la pantalla abierta\n * @param {Record<string, number>} [props.menuBadges] los conteos del servidor por badgeKey\n * @param {() => void} [props.onNavigate] al elegir una hija\n * @param {'side'|'above'} [props.placement] SUBMENU_PLACEMENT; por omisión a la derecha\n */\nexport default function SubmenuActionsMenu({anchorElement, onClose, menuItem, activeItem, menuBadges, onNavigate, placement = SUBMENU_PLACEMENT.side}) {\n const {t} = useToolkitTranslation('layout');\n const {Link: RouterLink} = useToolkitNavigation();\n const position = PLACEMENTS[placement] ?? PLACEMENTS[SUBMENU_PLACEMENT.side];\n\n return (\n <Menu anchorEl={anchorElement} open={Boolean(anchorElement)} onClose={onClose}\n anchorOrigin={position.anchorOrigin} transformOrigin={position.transformOrigin}\n slotProps={{paper: {sx: {minWidth: MIN_WIDTH_PIXELS, ...position.gapSx}}}}>\n {(menuItem.children ?? []).map((child) => {\n const ChildIcon = child.Icon;\n const badge = menuBadgeOf(child, menuBadges);\n return (\n <MenuItem key={child.to ?? child.href ?? menuItemLabelOf(child, t)} {...menuLinkPropsOf(child, RouterLink)} selected={child === activeItem}\n onClick={() => {\n onClose();\n onNavigate?.();\n }}>\n {ChildIcon && <ListItemIcon><ChildIcon fontSize=\"small\" /></ListItemIcon>}\n <ListItemText>{menuItemLabelOf(child, t)}</ListItemText>\n {badge && <Chip label={badge.content} size=\"small\" color=\"primary\" sx={{height: 20, ml: 1.5, ...textSizeOf(BADGE_PIXELS)}} />}\n </MenuItem>\n );\n })}\n </Menu>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;AAkBO,MAAM,oBAAoB,OAAO,OAAO,EAAC,MAAM,QAAQ,OAAO,SAAQ;AAG7E,MAAM,aAAa;AACnB,MAAM,mBAAmB;AACzB,MAAM,eAAe;AAErB,MAAM,aAAa,OAAO,OAAO;AAAA,EAC7B,CAAC,kBAAkB,IAAI,GAAG;AAAA,IACtB,cAAc,EAAC,UAAU,OAAO,YAAY,QAAA;AAAA,IAAU,iBAAiB,EAAC,UAAU,OAAO,YAAY,OAAA;AAAA,IAAS,OAAO,EAAC,IAAI,GAAG,UAAU,KAAA;AAAA,EAAI;AAAA,EAE/I,CAAC,kBAAkB,KAAK,GAAG;AAAA,IACvB,cAAc,EAAC,UAAU,OAAO,YAAY,SAAA;AAAA,IAAW,iBAAiB,EAAC,UAAU,UAAU,YAAY,SAAA;AAAA,IAAW,OAAO,EAAC,IAAI,IAAI,UAAU,KAAA;AAAA,EAAI;AAE1J,CAAC;AAYD,SAAwB,mBAAmB,EAAC,eAAe,SAAS,UAAU,YAAY,YAAY,YAAY,YAAY,kBAAkB,KAAA,GAAO;AACnJ,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ;AAC1C,QAAM,EAAC,MAAM,WAAA,IAAc,qBAAA;AAC3B,QAAM,WAAW,WAAW,SAAS,KAAK,WAAW,kBAAkB,IAAI;AAE3E,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAK,UAAU;AAAA,MAAe,MAAM,QAAQ,aAAa;AAAA,MAAG;AAAA,MACvD,cAAc,SAAS;AAAA,MAAc,iBAAiB,SAAS;AAAA,MAC/D,WAAW,EAAC,OAAO,EAAC,IAAI,EAAC,UAAU,kBAAkB,GAAG,SAAS,MAAA,IAAM;AAAA,MACvE,oBAAS,YAAY,CAAA,GAAI,IAAI,CAAC,UAAU;AACtC,cAAM,YAAY,MAAM;AACxB,cAAM,QAAQ,YAAY,OAAO,UAAU;AAC3C,eACI;AAAA,UAAC;AAAA,UAAA;AAAA,YAAoE,GAAG,gBAAgB,OAAO,UAAU;AAAA,YAAG,UAAU,UAAU;AAAA,YACtH,SAAS,MAAM;AACX,sBAAA;AACA,2BAAA;AAAA,YACJ;AAAA,YACL,UAAA;AAAA,cAAA,iCAAc,cAAA,EAAa,UAAA,oBAAC,WAAA,EAAU,UAAS,SAAQ,GAAE;AAAA,cAC1D,oBAAC,cAAA,EAAc,UAAA,gBAAgB,OAAO,CAAC,GAAE;AAAA,cACxC,6BAAU,MAAA,EAAK,OAAO,MAAM,SAAS,MAAK,SAAQ,OAAM,WAAU,IAAI,EAAC,QAAQ,IAAI,IAAI,KAAK,GAAG,WAAW,YAAY,IAAC,CAAG;AAAA,YAAA;AAAA,UAAA;AAAA,UAPhH,MAAM,MAAM,MAAM,QAAQ,gBAAgB,OAAO,CAAC;AAAA,QAAA;AAAA,MAUzE,CAAC;AAAA,IAAA;AAAA,EAAA;AAGb;"}
@@ -10,7 +10,7 @@ import ListItemText from "@mui/material/ListItemText";
10
10
  import Menu from "@mui/material/Menu";
11
11
  import MenuItem from "@mui/material/MenuItem";
12
12
  import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
13
- import { menuItemLabelOf } from "./menuItemText.js";
13
+ import { menuItemLabelOf, menuLinkPropsOf } from "./menuItemText.js";
14
14
  const ACTIVE_UNDERLINE_PIXELS = 2;
15
15
  function TopMenuEntry({ menuItem, activeItem }) {
16
16
  const { t } = useToolkitTranslation("layout");
@@ -31,8 +31,7 @@ function TopMenuEntry({ menuItem, activeItem }) {
31
31
  return /* @__PURE__ */ jsx(
32
32
  Button,
33
33
  {
34
- component: RouterLink,
35
- to: menuItem.to,
34
+ ...menuLinkPropsOf(menuItem, RouterLink),
36
35
  startIcon: Icon ? /* @__PURE__ */ jsx(Icon, { fontSize: "small" }) : void 0,
37
36
  "aria-current": isActive ? "page" : void 0,
38
37
  sx: buttonSx,
@@ -55,10 +54,10 @@ function TopMenuEntry({ menuItem, activeItem }) {
55
54
  ),
56
55
  /* @__PURE__ */ jsx(Menu, { anchorEl: anchorElement, open: Boolean(anchorElement), onClose: () => setAnchorElement(null), slotProps: { paper: { sx: { minWidth: 220, mt: 0.5 } } }, children: menuItem.children.map((child) => {
57
56
  const ChildIcon = child.Icon;
58
- return /* @__PURE__ */ jsxs(MenuItem, { component: RouterLink, to: child.to, selected: child === activeItem, onClick: () => setAnchorElement(null), children: [
57
+ return /* @__PURE__ */ jsxs(MenuItem, { ...menuLinkPropsOf(child, RouterLink), selected: child === activeItem, onClick: () => setAnchorElement(null), children: [
59
58
  ChildIcon && /* @__PURE__ */ jsx(ListItemIcon, { children: /* @__PURE__ */ jsx(ChildIcon, { fontSize: "small" }) }),
60
59
  /* @__PURE__ */ jsx(ListItemText, { primary: menuItemLabelOf(child, t) })
61
- ] }, child.to);
60
+ ] }, child.to ?? child.href);
62
61
  }) })
63
62
  ] });
64
63
  }
@@ -70,7 +69,7 @@ function TopMenu({ menuItems, activeItem }) {
70
69
  component: "nav",
71
70
  "aria-label": t("drawer.ariaLabel"),
72
71
  sx: { display: { xs: "none", md: "flex" }, alignItems: "stretch", alignSelf: "stretch", gap: 0.5, minWidth: 0, overflowX: "auto" },
73
- children: menuItems.map((menuItem) => /* @__PURE__ */ jsx(TopMenuEntry, { menuItem, activeItem }, menuItem.to ?? menuItemLabelOf(menuItem, t)))
72
+ children: menuItems.map((menuItem) => /* @__PURE__ */ jsx(TopMenuEntry, { menuItem, activeItem }, menuItem.to ?? menuItem.href ?? menuItemLabelOf(menuItem, t)))
74
73
  }
75
74
  );
76
75
  }
@@ -1 +1 @@
1
- {"version":3,"file":"TopMenu.js","sources":["../../src/appShell/TopMenu.jsx"],"sourcesContent":["// El menú dentro de la barra (menuPosition=\"top\"), para apps de pocos módulos: cada entrada es un botón; la que tiene\n// submenú abre una lista debajo. Las secciones no se pintan aquí (en una fila no hay dónde); en el teléfono el menú es\n// el mismo cajón del estilo lateral, con sus secciones.\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 Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\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 ExpandMoreIcon from '@mui/icons-material/ExpandMore';\n\nimport {menuItemLabelOf} from '@/appShell/menuItemText.js';\n\n// la entrada abierta se marca con una raya abajo, del color principal (en la barra naranja, del color del texto)\nconst ACTIVE_UNDERLINE_PIXELS = 2;\n\n/**\n * Un botón del menú de arriba, con su lista si tiene submenú.\n *\n * @param {{menuItem: object, activeItem: object|null}} props\n */\nfunction TopMenuEntry({menuItem, activeItem}) {\n const {t} = useToolkitTranslation('layout');\n const {Link: RouterLink} = useToolkitNavigation();\n const [anchorElement, setAnchorElement] = useState(null);\n const hasChildren = Array.isArray(menuItem.children) && menuItem.children.length > 0;\n const isActive = menuItem === activeItem || (hasChildren && menuItem.children.includes(activeItem));\n const Icon = menuItem.Icon;\n const buttonSx = {\n color: 'inherit', borderRadius: 1, px: 1.5, whiteSpace: 'nowrap', fontWeight: isActive ? 600 : 500,\n boxShadow: isActive ? `inset 0 -${ACTIVE_UNDERLINE_PIXELS}px 0 currentColor` : 'none',\n };\n\n if (!hasChildren) {\n return (\n <Button component={RouterLink} to={menuItem.to} startIcon={Icon ? <Icon fontSize=\"small\" /> : undefined}\n aria-current={isActive ? 'page' : undefined} sx={buttonSx}>\n {menuItemLabelOf(menuItem, t)}\n </Button>\n );\n }\n return (\n <>\n <Button onClick={(event) => setAnchorElement(event.currentTarget)} aria-haspopup=\"menu\" aria-expanded={Boolean(anchorElement)}\n startIcon={Icon ? <Icon fontSize=\"small\" /> : undefined} endIcon={<ExpandMoreIcon fontSize=\"small\" />} sx={buttonSx}>\n {menuItemLabelOf(menuItem, t)}\n </Button>\n <Menu anchorEl={anchorElement} open={Boolean(anchorElement)} onClose={() => setAnchorElement(null)} slotProps={{paper: {sx: {minWidth: 220, mt: 0.5}}}}>\n {menuItem.children.map((child) => {\n const ChildIcon = child.Icon;\n return (\n <MenuItem key={child.to} component={RouterLink} to={child.to} selected={child === activeItem} onClick={() => setAnchorElement(null)}>\n {ChildIcon && <ListItemIcon><ChildIcon fontSize=\"small\" /></ListItemIcon>}\n <ListItemText primary={menuItemLabelOf(child, t)} />\n </MenuItem>\n );\n })}\n </Menu>\n </>\n );\n}\n\n/**\n * @param {{menuItems: object[], activeItem: object|null}} props\n */\nexport default function TopMenu({menuItems, activeItem}) {\n const {t} = useToolkitTranslation('layout');\n return (\n <Box component=\"nav\" aria-label={t('drawer.ariaLabel')}\n sx={{display: {xs: 'none', md: 'flex'}, alignItems: 'stretch', alignSelf: 'stretch', gap: 0.5, minWidth: 0, overflowX: 'auto'}}>\n {menuItems.map((menuItem) => (\n <TopMenuEntry key={menuItem.to ?? menuItemLabelOf(menuItem, t)} menuItem={menuItem} activeItem={activeItem} />\n ))}\n </Box>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;AAmBA,MAAM,0BAA0B;AAOhC,SAAS,aAAa,EAAC,UAAU,cAAa;AAC1C,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ;AAC1C,QAAM,EAAC,MAAM,WAAA,IAAc,qBAAA;AAC3B,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,IAAI;AACvD,QAAM,cAAc,MAAM,QAAQ,SAAS,QAAQ,KAAK,SAAS,SAAS,SAAS;AACnF,QAAM,WAAW,aAAa,cAAe,eAAe,SAAS,SAAS,SAAS,UAAU;AACjG,QAAM,OAAO,SAAS;AACtB,QAAM,WAAW;AAAA,IACb,OAAO;AAAA,IAAW,cAAc;AAAA,IAAG,IAAI;AAAA,IAAK,YAAY;AAAA,IAAU,YAAY,WAAW,MAAM;AAAA,IAC/F,WAAW,WAAW,YAAY,uBAAuB,sBAAsB;AAAA,EAAA;AAGnF,MAAI,CAAC,aAAa;AACd,WACI;AAAA,MAAC;AAAA,MAAA;AAAA,QAAO,WAAW;AAAA,QAAY,IAAI,SAAS;AAAA,QAAI,WAAW,OAAO,oBAAC,MAAA,EAAK,UAAS,SAAQ,IAAK;AAAA,QACtF,gBAAc,WAAW,SAAS;AAAA,QAAW,IAAI;AAAA,QACpD,UAAA,gBAAgB,UAAU,CAAC;AAAA,MAAA;AAAA,IAAA;AAAA,EAGxC;AACA,SACI,qBAAA,UAAA,EACI,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QAAO,SAAS,CAAC,UAAU,iBAAiB,MAAM,aAAa;AAAA,QAAG,iBAAc;AAAA,QAAO,iBAAe,QAAQ,aAAa;AAAA,QACpH,WAAW,OAAO,oBAAC,MAAA,EAAK,UAAS,SAAQ,IAAK;AAAA,QAAW,SAAS,oBAAC,gBAAA,EAAe,UAAS,QAAA,CAAQ;AAAA,QAAI,IAAI;AAAA,QAC9G,UAAA,gBAAgB,UAAU,CAAC;AAAA,MAAA;AAAA,IAAA;AAAA,IAEhC,oBAAC,MAAA,EAAK,UAAU,eAAe,MAAM,QAAQ,aAAa,GAAG,SAAS,MAAM,iBAAiB,IAAI,GAAG,WAAW,EAAC,OAAO,EAAC,IAAI,EAAC,UAAU,KAAK,IAAI,IAAA,IAAI,GAC/I,UAAA,SAAS,SAAS,IAAI,CAAC,UAAU;AAC9B,YAAM,YAAY,MAAM;AACxB,aACI,qBAAC,UAAA,EAAwB,WAAW,YAAY,IAAI,MAAM,IAAI,UAAU,UAAU,YAAY,SAAS,MAAM,iBAAiB,IAAI,GAC7H,UAAA;AAAA,QAAA,iCAAc,cAAA,EAAa,UAAA,oBAAC,WAAA,EAAU,UAAS,SAAQ,GAAE;AAAA,4BACzD,cAAA,EAAa,SAAS,gBAAgB,OAAO,CAAC,EAAA,CAAG;AAAA,MAAA,EAAA,GAFvC,MAAM,EAGrB;AAAA,IAER,CAAC,EAAA,CACL;AAAA,EAAA,GACJ;AAER;AAKA,SAAwB,QAAQ,EAAC,WAAW,cAAa;AACrD,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ;AAC1C,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAI,WAAU;AAAA,MAAM,cAAY,EAAE,kBAAkB;AAAA,MAChD,IAAI,EAAC,SAAS,EAAC,IAAI,QAAQ,IAAI,OAAA,GAAS,YAAY,WAAW,WAAW,WAAW,KAAK,KAAK,UAAU,GAAG,WAAW,OAAA;AAAA,MACvH,UAAA,UAAU,IAAI,CAAC,iCACX,cAAA,EAA+D,UAAoB,WAAA,GAAjE,SAAS,MAAM,gBAAgB,UAAU,CAAC,CAA+C,CAC/G;AAAA,IAAA;AAAA,EAAA;AAGb;"}
1
+ {"version":3,"file":"TopMenu.js","sources":["../../src/appShell/TopMenu.jsx"],"sourcesContent":["// El menú dentro de la barra (menuPosition=\"top\"), para apps de pocos módulos: cada entrada es un botón; la que tiene\n// submenú abre una lista debajo. Las secciones no se pintan aquí (en una fila no hay dónde); en el teléfono el menú es\n// el mismo cajón del estilo lateral, con sus secciones.\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 Box from '@mui/material/Box';\nimport Button from '@mui/material/Button';\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 ExpandMoreIcon from '@mui/icons-material/ExpandMore';\n\nimport {menuItemLabelOf, menuLinkPropsOf} from '@/appShell/menuItemText.js';\n\n// la entrada abierta se marca con una raya abajo, del color principal (en la barra naranja, del color del texto)\nconst ACTIVE_UNDERLINE_PIXELS = 2;\n\n/**\n * Un botón del menú de arriba, con su lista si tiene submenú.\n *\n * @param {{menuItem: object, activeItem: object|null}} props\n */\nfunction TopMenuEntry({menuItem, activeItem}) {\n const {t} = useToolkitTranslation('layout');\n const {Link: RouterLink} = useToolkitNavigation();\n const [anchorElement, setAnchorElement] = useState(null);\n const hasChildren = Array.isArray(menuItem.children) && menuItem.children.length > 0;\n const isActive = menuItem === activeItem || (hasChildren && menuItem.children.includes(activeItem));\n const Icon = menuItem.Icon;\n const buttonSx = {\n color: 'inherit', borderRadius: 1, px: 1.5, whiteSpace: 'nowrap', fontWeight: isActive ? 600 : 500,\n boxShadow: isActive ? `inset 0 -${ACTIVE_UNDERLINE_PIXELS}px 0 currentColor` : 'none',\n };\n\n if (!hasChildren) {\n return (\n <Button {...menuLinkPropsOf(menuItem, RouterLink)} startIcon={Icon ? <Icon fontSize=\"small\" /> : undefined}\n aria-current={isActive ? 'page' : undefined} sx={buttonSx}>\n {menuItemLabelOf(menuItem, t)}\n </Button>\n );\n }\n return (\n <>\n <Button onClick={(event) => setAnchorElement(event.currentTarget)} aria-haspopup=\"menu\" aria-expanded={Boolean(anchorElement)}\n startIcon={Icon ? <Icon fontSize=\"small\" /> : undefined} endIcon={<ExpandMoreIcon fontSize=\"small\" />} sx={buttonSx}>\n {menuItemLabelOf(menuItem, t)}\n </Button>\n <Menu anchorEl={anchorElement} open={Boolean(anchorElement)} onClose={() => setAnchorElement(null)} slotProps={{paper: {sx: {minWidth: 220, mt: 0.5}}}}>\n {menuItem.children.map((child) => {\n const ChildIcon = child.Icon;\n return (\n <MenuItem key={child.to ?? child.href} {...menuLinkPropsOf(child, RouterLink)} selected={child === activeItem} onClick={() => setAnchorElement(null)}>\n {ChildIcon && <ListItemIcon><ChildIcon fontSize=\"small\" /></ListItemIcon>}\n <ListItemText primary={menuItemLabelOf(child, t)} />\n </MenuItem>\n );\n })}\n </Menu>\n </>\n );\n}\n\n/**\n * @param {{menuItems: object[], activeItem: object|null}} props\n */\nexport default function TopMenu({menuItems, activeItem}) {\n const {t} = useToolkitTranslation('layout');\n return (\n <Box component=\"nav\" aria-label={t('drawer.ariaLabel')}\n sx={{display: {xs: 'none', md: 'flex'}, alignItems: 'stretch', alignSelf: 'stretch', gap: 0.5, minWidth: 0, overflowX: 'auto'}}>\n {menuItems.map((menuItem) => (\n <TopMenuEntry key={menuItem.to ?? menuItem.href ?? menuItemLabelOf(menuItem, t)} menuItem={menuItem} activeItem={activeItem} />\n ))}\n </Box>\n );\n}\n"],"names":[],"mappings":";;;;;;;;;;;;;AAmBA,MAAM,0BAA0B;AAOhC,SAAS,aAAa,EAAC,UAAU,cAAa;AAC1C,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ;AAC1C,QAAM,EAAC,MAAM,WAAA,IAAc,qBAAA;AAC3B,QAAM,CAAC,eAAe,gBAAgB,IAAI,SAAS,IAAI;AACvD,QAAM,cAAc,MAAM,QAAQ,SAAS,QAAQ,KAAK,SAAS,SAAS,SAAS;AACnF,QAAM,WAAW,aAAa,cAAe,eAAe,SAAS,SAAS,SAAS,UAAU;AACjG,QAAM,OAAO,SAAS;AACtB,QAAM,WAAW;AAAA,IACb,OAAO;AAAA,IAAW,cAAc;AAAA,IAAG,IAAI;AAAA,IAAK,YAAY;AAAA,IAAU,YAAY,WAAW,MAAM;AAAA,IAC/F,WAAW,WAAW,YAAY,uBAAuB,sBAAsB;AAAA,EAAA;AAGnF,MAAI,CAAC,aAAa;AACd,WACI;AAAA,MAAC;AAAA,MAAA;AAAA,QAAQ,GAAG,gBAAgB,UAAU,UAAU;AAAA,QAAG,WAAW,OAAO,oBAAC,MAAA,EAAK,UAAS,SAAQ,IAAK;AAAA,QACzF,gBAAc,WAAW,SAAS;AAAA,QAAW,IAAI;AAAA,QACpD,UAAA,gBAAgB,UAAU,CAAC;AAAA,MAAA;AAAA,IAAA;AAAA,EAGxC;AACA,SACI,qBAAA,UAAA,EACI,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QAAO,SAAS,CAAC,UAAU,iBAAiB,MAAM,aAAa;AAAA,QAAG,iBAAc;AAAA,QAAO,iBAAe,QAAQ,aAAa;AAAA,QACpH,WAAW,OAAO,oBAAC,MAAA,EAAK,UAAS,SAAQ,IAAK;AAAA,QAAW,SAAS,oBAAC,gBAAA,EAAe,UAAS,QAAA,CAAQ;AAAA,QAAI,IAAI;AAAA,QAC9G,UAAA,gBAAgB,UAAU,CAAC;AAAA,MAAA;AAAA,IAAA;AAAA,IAEhC,oBAAC,MAAA,EAAK,UAAU,eAAe,MAAM,QAAQ,aAAa,GAAG,SAAS,MAAM,iBAAiB,IAAI,GAAG,WAAW,EAAC,OAAO,EAAC,IAAI,EAAC,UAAU,KAAK,IAAI,IAAA,IAAI,GAC/I,UAAA,SAAS,SAAS,IAAI,CAAC,UAAU;AAC9B,YAAM,YAAY,MAAM;AACxB,aACI,qBAAC,UAAA,EAAuC,GAAG,gBAAgB,OAAO,UAAU,GAAG,UAAU,UAAU,YAAY,SAAS,MAAM,iBAAiB,IAAI,GAC9I,UAAA;AAAA,QAAA,iCAAc,cAAA,EAAa,UAAA,oBAAC,WAAA,EAAU,UAAS,SAAQ,GAAE;AAAA,4BACzD,cAAA,EAAa,SAAS,gBAAgB,OAAO,CAAC,EAAA,CAAG;AAAA,MAAA,EAAA,GAFvC,MAAM,MAAM,MAAM,IAGjC;AAAA,IAER,CAAC,EAAA,CACL;AAAA,EAAA,GACJ;AAER;AAKA,SAAwB,QAAQ,EAAC,WAAW,cAAa;AACrD,QAAM,EAAC,EAAA,IAAK,sBAAsB,QAAQ;AAC1C,SACI;AAAA,IAAC;AAAA,IAAA;AAAA,MAAI,WAAU;AAAA,MAAM,cAAY,EAAE,kBAAkB;AAAA,MAChD,IAAI,EAAC,SAAS,EAAC,IAAI,QAAQ,IAAI,OAAA,GAAS,YAAY,WAAW,WAAW,WAAW,KAAK,KAAK,UAAU,GAAG,WAAW,OAAA;AAAA,MACvH,oBAAU,IAAI,CAAC,aACZ,oBAAC,gBAAgF,UAAoB,WAAA,GAAlF,SAAS,MAAM,SAAS,QAAQ,gBAAgB,UAAU,CAAC,CAA+C,CAChI;AAAA,IAAA;AAAA,EAAA;AAGb;"}
@@ -0,0 +1,11 @@
1
+ "use client";
2
+ import { jsx } from "react/jsx-runtime";
3
+ import Box from "@mui/material/Box";
4
+ const WORDMARK_HEIGHT_PIXELS = 24;
5
+ function Wordmark({ children }) {
6
+ return /* @__PURE__ */ jsx(Box, { sx: { height: WORDMARK_HEIGHT_PIXELS, display: "flex", alignItems: "center", flex: "0 0 auto", "& > img, & > svg": { height: "100%", width: "auto", display: "block" } }, children });
7
+ }
8
+ export {
9
+ Wordmark as default
10
+ };
11
+ //# sourceMappingURL=Wordmark.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Wordmark.js","sources":["../../src/appShell/Wordmark.jsx"],"sourcesContent":["// El logo ancho de la app (la marca con su nombre en una sola imagen, como el de muchas empresas) en la barra y arriba del\n// menú del teléfono: a 24 px de alto y lo ancho que pida, sin deformarse. Una imagen o un svg sueltos toman ese alto solos.\n//\n// Genérico del paquete: function (regla de la casa).\nimport Box from '@mui/material/Box';\n\n// el alto del logo ancho: el de la letra del nombre con su renglón, para que la barra no crezca\nconst WORDMARK_HEIGHT_PIXELS = 24;\n\n/**\n * @param {{children: import('react').ReactNode}} props el logo (una <img>, un <svg> o lo que pinte el proyecto)\n */\nexport default function Wordmark({children}) {\n return (\n <Box sx={{height: WORDMARK_HEIGHT_PIXELS, display: 'flex', alignItems: 'center', flex: '0 0 auto', '& > img, & > svg': {height: '100%', width: 'auto', display: 'block'}}}>\n {children}\n </Box>\n );\n}\n"],"names":[],"mappings":";;;AAOA,MAAM,yBAAyB;AAK/B,SAAwB,SAAS,EAAC,YAAW;AACzC,SACI,oBAAC,OAAI,IAAI,EAAC,QAAQ,wBAAwB,SAAS,QAAQ,YAAY,UAAU,MAAM,YAAY,oBAAoB,EAAC,QAAQ,QAAQ,OAAO,QAAQ,SAAS,QAAA,KAC3J,UACL;AAER;"}
@@ -23,11 +23,15 @@ function initialsOf(fullName) {
23
23
  const words = String(fullName ?? "").trim().split(/\s+/).filter((word) => Boolean(word) && !NAME_PARTICLES.has(word.toLowerCase()));
24
24
  return words.slice(0, 2).map((word) => word[0]?.toUpperCase() ?? "").join("") || "?";
25
25
  }
26
+ function menuLinkPropsOf(menuItem, RouterLink) {
27
+ return menuItem.href ? { component: "a", href: menuItem.href } : { component: RouterLink, to: menuItem.to };
28
+ }
26
29
  export {
27
30
  initialsOf,
28
31
  menuItemDescriptionOf,
29
32
  menuItemLabelOf,
30
33
  menuItemSectionOf,
34
+ menuLinkPropsOf,
31
35
  menuSectionsOf
32
36
  };
33
37
  //# sourceMappingURL=menuItemText.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"menuItemText.js","sources":["../../src/appShell/menuItemText.js"],"sourcesContent":["// Los textos de una entrada del menú y de la persona en la barra, ya listos para pintarse.\n//\n// Una entrada del menú puede traer su texto escrito (label, description, section) o la llave para traducirlo (labelKey,\n// descriptionKey, sectionKey): con llave, cambia de idioma junto con la app.\n//\n// Funciones puras, sin React: function (regla de la casa). Se prueban con node --test (menuItemText.test.js).\n\n/**\n * El nombre de una entrada del menú.\n *\n * @param {{label?: string, labelKey?: string}} menuItem\n * @param {(key: string) => string} t el traductor de la pantalla\n * @returns {string}\n */\nexport function menuItemLabelOf(menuItem, t) {\n return menuItem.label ?? (menuItem.labelKey ? t(menuItem.labelKey) : '');\n}\n\n/**\n * Para qué sirve una entrada (para las tarjetas de un inicio).\n *\n * @param {{description?: string, descriptionKey?: string}} menuItem\n * @param {(key: string) => string} t\n * @returns {string|undefined}\n */\nexport function menuItemDescriptionOf(menuItem, t) {\n return menuItem.description ?? (menuItem.descriptionKey ? t(menuItem.descriptionKey) : undefined);\n}\n\n/**\n * El título de la sección del menú lateral donde va la entrada («Operación», «Administración»).\n *\n * @param {{section?: string, sectionKey?: string}} menuItem\n * @param {(key: string) => string} t\n * @returns {string|undefined}\n */\nexport function menuItemSectionOf(menuItem, t) {\n return menuItem.section ?? (menuItem.sectionKey ? t(menuItem.sectionKey) : undefined);\n}\n\n/**\n * Las entradas juntas por sección: las secciones salen en el orden en que aparece su primera entrada, y adentro cada\n * entrada en su orden. Las entradas sin sección forman su propio bloque sin título.\n *\n * Por qué por título y no por vecindad: con vecindad, dos módulos de la misma sección con un `order` de otra en medio\n * partían la sección en dos («Interfaz» salía dos veces). Así cada módulo dice su sección sin coordinar números.\n *\n * @param {object[]} menuItems lo que regresa composeMenu\n * @param {(key: string) => string} t\n * @returns {Array<{title: string|undefined, menuItems: object[]}>}\n */\nexport function menuSectionsOf(menuItems, t) {\n const sectionsByTitle = new Map();\n menuItems.forEach((menuItem) => {\n const title = menuItemSectionOf(menuItem, t);\n if (!sectionsByTitle.has(title)) {\n sectionsByTitle.set(title, {title, menuItems: []});\n }\n sectionsByTitle.get(title).menuItems.push(menuItem);\n });\n return [...sectionsByTitle.values()];\n}\n\n// las partículas de los nombres («María de la Luz», «Juan van Dijk»): no son iniciales. Con ellas, «Usuario de Ejemplo»\n// salía «UD» en el avatar.\nconst NAME_PARTICLES = new Set(['de', 'del', 'la', 'las', 'los', 'y', 'e', 'da', 'das', 'do', 'dos', 'di', 'du', 'van', 'von', 'der', 'le']);\n\n/**\n * Las iniciales para el avatar: «Ana María López» → «AM», «Usuario de Ejemplo» → «UE». Sin nombre, «?».\n *\n * @param {string|null|undefined} fullName\n * @returns {string}\n */\nexport function initialsOf(fullName) {\n const words = String(fullName ?? '').trim().split(/\\s+/).filter((word) => Boolean(word) && !NAME_PARTICLES.has(word.toLowerCase()));\n return words.slice(0, 2).map((word) => word[0]?.toUpperCase() ?? '').join('') || '?';\n}\n"],"names":[],"mappings":"AAcO,SAAS,gBAAgB,UAAU,GAAG;AACzC,SAAO,SAAS,UAAU,SAAS,WAAW,EAAE,SAAS,QAAQ,IAAI;AACzE;AASO,SAAS,sBAAsB,UAAU,GAAG;AAC/C,SAAO,SAAS,gBAAgB,SAAS,iBAAiB,EAAE,SAAS,cAAc,IAAI;AAC3F;AASO,SAAS,kBAAkB,UAAU,GAAG;AAC3C,SAAO,SAAS,YAAY,SAAS,aAAa,EAAE,SAAS,UAAU,IAAI;AAC/E;AAaO,SAAS,eAAe,WAAW,GAAG;AACzC,QAAM,kBAAkB,oBAAI,IAAG;AAC/B,YAAU,QAAQ,CAAC,aAAa;AAC5B,UAAM,QAAQ,kBAAkB,UAAU,CAAC;AAC3C,QAAI,CAAC,gBAAgB,IAAI,KAAK,GAAG;AAC7B,sBAAgB,IAAI,OAAO,EAAC,OAAO,WAAW,CAAA,EAAE,CAAC;AAAA,IACrD;AACA,oBAAgB,IAAI,KAAK,EAAE,UAAU,KAAK,QAAQ;AAAA,EACtD,CAAC;AACD,SAAO,CAAC,GAAG,gBAAgB,QAAQ;AACvC;AAIA,MAAM,iBAAiB,oBAAI,IAAI,CAAC,MAAM,OAAO,MAAM,OAAO,OAAO,KAAK,KAAK,MAAM,OAAO,MAAM,OAAO,MAAM,MAAM,OAAO,OAAO,OAAO,IAAI,CAAC;AAQpI,SAAS,WAAW,UAAU;AACjC,QAAM,QAAQ,OAAO,YAAY,EAAE,EAAE,KAAI,EAAG,MAAM,KAAK,EAAE,OAAO,CAAC,SAAS,QAAQ,IAAI,KAAK,CAAC,eAAe,IAAI,KAAK,YAAW,CAAE,CAAC;AAClI,SAAO,MAAM,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,SAAS,KAAK,CAAC,GAAG,iBAAiB,EAAE,EAAE,KAAK,EAAE,KAAK;AACrF;"}
1
+ {"version":3,"file":"menuItemText.js","sources":["../../src/appShell/menuItemText.js"],"sourcesContent":["// Los textos de una entrada del menú y de la persona en la barra, ya listos para pintarse.\n//\n// Una entrada del menú puede traer su texto escrito (label, description, section) o la llave para traducirlo (labelKey,\n// descriptionKey, sectionKey): con llave, cambia de idioma junto con la app.\n//\n// Funciones puras, sin React: function (regla de la casa). Se prueban con node --test (menuItemText.test.js).\n\n/**\n * El nombre de una entrada del menú.\n *\n * @param {{label?: string, labelKey?: string}} menuItem\n * @param {(key: string) => string} t el traductor de la pantalla\n * @returns {string}\n */\nexport function menuItemLabelOf(menuItem, t) {\n return menuItem.label ?? (menuItem.labelKey ? t(menuItem.labelKey) : '');\n}\n\n/**\n * Para qué sirve una entrada (para las tarjetas de un inicio).\n *\n * @param {{description?: string, descriptionKey?: string}} menuItem\n * @param {(key: string) => string} t\n * @returns {string|undefined}\n */\nexport function menuItemDescriptionOf(menuItem, t) {\n return menuItem.description ?? (menuItem.descriptionKey ? t(menuItem.descriptionKey) : undefined);\n}\n\n/**\n * El título de la sección del menú lateral donde va la entrada («Operación», «Administración»).\n *\n * @param {{section?: string, sectionKey?: string}} menuItem\n * @param {(key: string) => string} t\n * @returns {string|undefined}\n */\nexport function menuItemSectionOf(menuItem, t) {\n return menuItem.section ?? (menuItem.sectionKey ? t(menuItem.sectionKey) : undefined);\n}\n\n/**\n * Las entradas juntas por sección: las secciones salen en el orden en que aparece su primera entrada, y adentro cada\n * entrada en su orden. Las entradas sin sección forman su propio bloque sin título.\n *\n * Por qué por título y no por vecindad: con vecindad, dos módulos de la misma sección con un `order` de otra en medio\n * partían la sección en dos («Interfaz» salía dos veces). Así cada módulo dice su sección sin coordinar números.\n *\n * @param {object[]} menuItems lo que regresa composeMenu\n * @param {(key: string) => string} t\n * @returns {Array<{title: string|undefined, menuItems: object[]}>}\n */\nexport function menuSectionsOf(menuItems, t) {\n const sectionsByTitle = new Map();\n menuItems.forEach((menuItem) => {\n const title = menuItemSectionOf(menuItem, t);\n if (!sectionsByTitle.has(title)) {\n sectionsByTitle.set(title, {title, menuItems: []});\n }\n sectionsByTitle.get(title).menuItems.push(menuItem);\n });\n return [...sectionsByTitle.values()];\n}\n\n// las partículas de los nombres («María de la Luz», «Juan van Dijk»): no son iniciales. Con ellas, «Usuario de Ejemplo»\n// salía «UD» en el avatar.\nconst NAME_PARTICLES = new Set(['de', 'del', 'la', 'las', 'los', 'y', 'e', 'da', 'das', 'do', 'dos', 'di', 'du', 'van', 'von', 'der', 'le']);\n\n/**\n * Las iniciales para el avatar: «Ana María López» → «AM», «Usuario de Ejemplo» → «UE». Sin nombre, «?».\n *\n * @param {string|null|undefined} fullName\n * @returns {string}\n */\nexport function initialsOf(fullName) {\n const words = String(fullName ?? '').trim().split(/\\s+/).filter((word) => Boolean(word) && !NAME_PARTICLES.has(word.toLowerCase()));\n return words.slice(0, 2).map((word) => word[0]?.toUpperCase() ?? '').join('') || '?';\n}\n\n/**\n * Las props de la liga de una entrada: el router para una pantalla de la app (to) y una dirección normal para otro sistema\n * (href: el sistema que ya existía, otra app de la empresa). Con href la página se carga completa, como cualquier liga.\n *\n * @param {{to?: string, href?: string}} menuItem\n * @param {import('react').ElementType} RouterLink el Link de la navegación del proyecto\n * @returns {{component: import('react').ElementType|string, to?: string, href?: string}}\n */\nexport function menuLinkPropsOf(menuItem, RouterLink) {\n return menuItem.href ? {component: 'a', href: menuItem.href} : {component: RouterLink, to: menuItem.to};\n}\n"],"names":[],"mappings":"AAcO,SAAS,gBAAgB,UAAU,GAAG;AACzC,SAAO,SAAS,UAAU,SAAS,WAAW,EAAE,SAAS,QAAQ,IAAI;AACzE;AASO,SAAS,sBAAsB,UAAU,GAAG;AAC/C,SAAO,SAAS,gBAAgB,SAAS,iBAAiB,EAAE,SAAS,cAAc,IAAI;AAC3F;AASO,SAAS,kBAAkB,UAAU,GAAG;AAC3C,SAAO,SAAS,YAAY,SAAS,aAAa,EAAE,SAAS,UAAU,IAAI;AAC/E;AAaO,SAAS,eAAe,WAAW,GAAG;AACzC,QAAM,kBAAkB,oBAAI,IAAG;AAC/B,YAAU,QAAQ,CAAC,aAAa;AAC5B,UAAM,QAAQ,kBAAkB,UAAU,CAAC;AAC3C,QAAI,CAAC,gBAAgB,IAAI,KAAK,GAAG;AAC7B,sBAAgB,IAAI,OAAO,EAAC,OAAO,WAAW,CAAA,EAAE,CAAC;AAAA,IACrD;AACA,oBAAgB,IAAI,KAAK,EAAE,UAAU,KAAK,QAAQ;AAAA,EACtD,CAAC;AACD,SAAO,CAAC,GAAG,gBAAgB,QAAQ;AACvC;AAIA,MAAM,iBAAiB,oBAAI,IAAI,CAAC,MAAM,OAAO,MAAM,OAAO,OAAO,KAAK,KAAK,MAAM,OAAO,MAAM,OAAO,MAAM,MAAM,OAAO,OAAO,OAAO,IAAI,CAAC;AAQpI,SAAS,WAAW,UAAU;AACjC,QAAM,QAAQ,OAAO,YAAY,EAAE,EAAE,KAAI,EAAG,MAAM,KAAK,EAAE,OAAO,CAAC,SAAS,QAAQ,IAAI,KAAK,CAAC,eAAe,IAAI,KAAK,YAAW,CAAE,CAAC;AAClI,SAAO,MAAM,MAAM,GAAG,CAAC,EAAE,IAAI,CAAC,SAAS,KAAK,CAAC,GAAG,iBAAiB,EAAE,EAAE,KAAK,EAAE,KAAK;AACrF;AAUO,SAAS,gBAAgB,UAAU,YAAY;AAClD,SAAO,SAAS,OAAO,EAAC,WAAW,KAAK,MAAM,SAAS,KAAI,IAAI,EAAC,WAAW,YAAY,IAAI,SAAS,GAAE;AAC1G;"}
package/dist/index.js CHANGED
@@ -21,7 +21,7 @@ import { DEFAULT_FILE_TYPES, UNKNOWN_FILE_TYPE, useFileTypes } from "./downloads
21
21
  import { DOWNLOAD_STATUS, downloadsMachine } from "./downloads/downloadsMachine.js";
22
22
  import { THEME_LOOK, TOP_BAR, createToolkitTheme } from "./theme/createToolkitTheme.js";
23
23
  import { isAndroidDevice } from "./theme/androidDevice.js";
24
- import { MINIMAL_BACKGROUND } from "./theme/components/minimalComponents.js";
24
+ import { MINIMAL_BACKGROUND, MINIMAL_TONES } from "./theme/components/minimalComponents.js";
25
25
  import { STACKCRAFT, stackcraftDarkPalette, stackcraftLightPalette } from "./theme/stackcraftPalette.js";
26
26
  import { toolkitComponents } from "./theme/toolkitComponents.js";
27
27
  import { mergeComponents } from "./theme/mergeComponents.js";
@@ -42,7 +42,7 @@ import { DRAWER_WIDTH_COLLAPSED, DRAWER_WIDTH_EXPANDED, MAIN_CONTENT_ID } from "
42
42
  import { useAppMenu } from "./appShell/appMenuContext.js";
43
43
  import { usePageTitle } from "./appShell/pageTitle.js";
44
44
  import { initialsOf, menuItemDescriptionOf, menuItemLabelOf, menuItemSectionOf, menuSectionsOf } from "./appShell/menuItemText.js";
45
- import { activeMenuItemOf, composeMenu, menuBadgeOf, menuBreadcrumbsOf, mobileTabsOf, parentMenuItemOf, screenMenuItemsOf } from "./navigation/appMenu.js";
45
+ import { activeMenuItemOf, composeMenu, menuBadgeOf, menuBreadcrumbsOf, menuItemsWithIconsOf, mobileTabsOf, parentMenuItemOf, screenMenuItemsOf } from "./navigation/appMenu.js";
46
46
  import { menuBadgesFrom, useMenuBadges } from "./navigation/useMenuBadges.js";
47
47
  import { allowOnlyUnrestricted, createAccessChecker } from "./access/accessRules.js";
48
48
  import { default as default18 } from "./access/Can.js";
@@ -112,12 +112,12 @@ import { REDIRECT_PARAM, readRedirectParam, safeInternalPath, withRedirect } fro
112
112
  import { recallListUrl, rememberListUrl, useRememberListUrl } from "./lastListUrl.js";
113
113
  import { currentHistoryIndex, forgetPageName, pageNameAt, rememberPageName, useNavigationTrail, usePageName } from "./navigationTrail.js";
114
114
  import { periodRangeIsValid, rangeEndAfterStartChange, rangeEndOptionsOf } from "./periodRanges.js";
115
- import { createLogger, setLogLevel } from "./observability/logger.js";
115
+ import { addLogSink, createLogger, setLogLevel } from "./observability/logger.js";
116
116
  import { isNewBuildPublished, reportPublishedBuild, useNewBuildPublished } from "./observability/publishedBuild.js";
117
117
  import { BUILD_WATCH_ACTION, buildWatchActionOf, publishedBuildIdOf } from "./observability/buildWatch.js";
118
118
  import { usePublishedBuildWatch } from "./observability/usePublishedBuildWatch.js";
119
119
  import { API_ERROR_STATUS, ApiError } from "./api/ApiError.js";
120
- import { API_RESPONSE_PARSE, apiUrlOf, createApiClient } from "./api/createApiClient.js";
120
+ import { API_RESPONSE_PARSE, apiErrorOf, apiUrlOf, createApiClient } from "./api/createApiClient.js";
121
121
  import { isStaleChunkError, recoverFromStaleChunk } from "./api/staleChunks.js";
122
122
  import { ACCOUNT_EVENT_KIND, default as default63 } from "./security/ActivityLog.js";
123
123
  import { default as default64 } from "./security/RecoveryCodesDialog.js";
@@ -205,6 +205,7 @@ export {
205
205
  MAIN_CONTENT_ID,
206
206
  MENU_POSITION,
207
207
  MINIMAL_BACKGROUND,
208
+ MINIMAL_TONES,
208
209
  MIN_CONTRAST,
209
210
  default67 as MessageList,
210
211
  default68 as MessageView,
@@ -253,8 +254,10 @@ export {
253
254
  XLSX_CONTENT_TYPE,
254
255
  accentTextStylesOf,
255
256
  activeMenuItemOf,
257
+ addLogSink,
256
258
  allowOnlyUnrestricted,
257
259
  ambientBackgroundOf,
260
+ apiErrorOf,
258
261
  apiUrlOf,
259
262
  barMenuPlacementOf,
260
263
  browserNavigation,
@@ -325,6 +328,7 @@ export {
325
328
  menuItemDescriptionOf,
326
329
  menuItemLabelOf,
327
330
  menuItemSectionOf,
331
+ menuItemsWithIconsOf,
328
332
  menuSectionsOf,
329
333
  mergeComponents,
330
334
  mobileTabsOf,
@@ -2,7 +2,18 @@ const LAST_ORDER = 1e3;
2
2
  const HOME_PATH = "/";
3
3
  function composeMenu(menuModules, { canAccess = () => true, context = {} } = {}) {
4
4
  const modules = Array.isArray(menuModules) ? menuModules : Object.values(menuModules);
5
- return modules.filter((menuModule) => canAccess(menuModule.access)).sort((first, second) => (first.order ?? LAST_ORDER) - (second.order ?? LAST_ORDER)).flatMap((menuModule) => menuModule.buildMenuItems?.(context) ?? [menuModule.buildMenuItem?.(context)]).filter((menuItem) => Boolean(menuItem) && canAccess(menuItem.access)).map((menuItem) => withAllowedChildren(menuItem, canAccess)).filter((menuItem) => Boolean(menuItem.to) || menuItem.children?.length > 0);
5
+ return modules.filter((menuModule) => canAccess(menuModule.access)).sort((first, second) => (first.order ?? LAST_ORDER) - (second.order ?? LAST_ORDER)).flatMap((menuModule) => menuModule.buildMenuItems?.(context) ?? [menuModule.buildMenuItem?.(context)]).filter((menuItem) => Boolean(menuItem) && canAccess(menuItem.access)).map((menuItem) => withAllowedChildren(menuItem, canAccess)).filter((menuItem) => Boolean(menuItem.to) || Boolean(menuItem.href) || menuItem.children?.length > 0);
6
+ }
7
+ function menuItemsWithIconsOf(menuItems) {
8
+ return menuItems.map((menuItem) => {
9
+ const Icon = menuItem.Icon ?? (menuItem.icon ? iconComponentOf(menuItem.icon) : void 0);
10
+ const children = Array.isArray(menuItem.children) ? menuItemsWithIconsOf(menuItem.children) : menuItem.children;
11
+ return { ...menuItem, Icon, children };
12
+ });
13
+ }
14
+ function iconComponentOf(icon) {
15
+ const MenuIcon = () => icon;
16
+ return MenuIcon;
6
17
  }
7
18
  function withAllowedChildren(menuItem, canAccess) {
8
19
  if (!Array.isArray(menuItem.children)) {
@@ -86,6 +97,7 @@ export {
86
97
  menuBadgeOf,
87
98
  menuBreadcrumbsOf,
88
99
  menuItemKeyOf,
100
+ menuItemsWithIconsOf,
89
101
  mobileTabsOf,
90
102
  parentMenuItemOf,
91
103
  screenMenuItemsOf