@nextelco/common-ui 2.1.106 → 2.1.107

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 (177) hide show
  1. package/dist/esm/index.js +201 -145
  2. package/dist/esm/index.js.map +1 -1
  3. package/dist/esm/legacy/components/atoms/InputData/InputData.js +2 -13
  4. package/dist/esm/legacy/components/atoms/InputData/InputData.js.map +1 -1
  5. package/dist/esm/legacy/components/charts/Charts.js +147 -57
  6. package/dist/esm/legacy/components/charts/Charts.js.map +1 -1
  7. package/dist/esm/legacy/components/layout/Layout/Layout.js +21 -82
  8. package/dist/esm/legacy/components/layout/Layout/Layout.js.map +1 -1
  9. package/dist/esm/legacy/components/layout/Sidebar/Sidebar.js +29 -218
  10. package/dist/esm/legacy/components/layout/Sidebar/Sidebar.js.map +1 -1
  11. package/dist/esm/legacy/components/molecules/ColumnPicker/ColumnPicker.js +77 -10
  12. package/dist/esm/legacy/components/molecules/ColumnPicker/ColumnPicker.js.map +1 -1
  13. package/dist/esm/legacy/components/molecules/DataTable/DataTable.js +445 -337
  14. package/dist/esm/legacy/components/molecules/DataTable/DataTable.js.map +1 -1
  15. package/dist/esm/legacy/components/molecules/UserAvatar/UserAvatar.js +15 -97
  16. package/dist/esm/legacy/components/molecules/UserAvatar/UserAvatar.js.map +1 -1
  17. package/dist/esm/legacy/components/organisms/MapaPontos/MapaPontos.js +3 -3
  18. package/dist/esm/legacy/components/organisms/MapaPontos/MapaPontos.js.map +1 -1
  19. package/dist/esm/legacy/components/organisms/PainelFiltros/PainelFiltros.js +15 -3
  20. package/dist/esm/legacy/components/organisms/PainelFiltros/PainelFiltros.js.map +1 -1
  21. package/dist/esm/legacy/components/organisms/Selector/Selector.js +4 -4
  22. package/dist/esm/legacy/components/organisms/Selector/Selector.js.map +1 -1
  23. package/dist/esm/legacy/components/ui/Avisos.js +23 -76
  24. package/dist/esm/legacy/components/ui/Avisos.js.map +1 -1
  25. package/dist/esm/legacy/components/ui/CabecalhoPagina.js +14 -27
  26. package/dist/esm/legacy/components/ui/CabecalhoPagina.js.map +1 -1
  27. package/dist/esm/legacy/components/ui/CaixaPesquisa.js +3 -51
  28. package/dist/esm/legacy/components/ui/CaixaPesquisa.js.map +1 -1
  29. package/dist/esm/legacy/components/ui/Confirmacao.js +13 -31
  30. package/dist/esm/legacy/components/ui/Confirmacao.js.map +1 -1
  31. package/dist/esm/legacy/components/ui/Escolha.js +21 -137
  32. package/dist/esm/legacy/components/ui/Escolha.js.map +1 -1
  33. package/dist/esm/legacy/components/ui/FaixaAviso.js +13 -11
  34. package/dist/esm/legacy/components/ui/FaixaAviso.js.map +1 -1
  35. package/dist/esm/legacy/components/ui/Separadores.js +11 -46
  36. package/dist/esm/legacy/components/ui/Separadores.js.map +1 -1
  37. package/dist/esm/legacy/components/ui/Ui.js +18 -115
  38. package/dist/esm/legacy/components/ui/Ui.js.map +1 -1
  39. package/dist/esm/legacy/components/ui/painelFlutuante.js +5 -44
  40. package/dist/esm/legacy/components/ui/painelFlutuante.js.map +1 -1
  41. package/dist/esm/v1/capabilities/overlay/useFloatingPanel.js +52 -0
  42. package/dist/esm/v1/capabilities/overlay/useFloatingPanel.js.map +1 -0
  43. package/dist/esm/v1/capabilities/theme/useIsDark.js +15 -0
  44. package/dist/esm/v1/capabilities/theme/useIsDark.js.map +1 -0
  45. package/dist/esm/v1/capabilities/theme/useTheme.js +66 -0
  46. package/dist/esm/v1/capabilities/theme/useTheme.js.map +1 -0
  47. package/dist/esm/v1/components/data/Pagination/Pagination.js +66 -0
  48. package/dist/esm/v1/components/data/Pagination/Pagination.js.map +1 -0
  49. package/dist/esm/v1/components/feedback/Toast/Toast.js +105 -0
  50. package/dist/esm/v1/components/feedback/Toast/Toast.js.map +1 -0
  51. package/dist/esm/v1/components/layout/AppShell/AppShell.js +89 -0
  52. package/dist/esm/v1/components/layout/AppShell/AppShell.js.map +1 -0
  53. package/dist/esm/v1/components/layout/Page/Page.js +16 -0
  54. package/dist/esm/v1/components/layout/Page/Page.js.map +1 -0
  55. package/dist/esm/v1/components/layout/PageHeader/PageHeader.js +26 -0
  56. package/dist/esm/v1/components/layout/PageHeader/PageHeader.js.map +1 -0
  57. package/dist/esm/v1/components/layout/SideNav/SideNav.js +277 -0
  58. package/dist/esm/v1/components/layout/SideNav/SideNav.js.map +1 -0
  59. package/dist/esm/v1/components/layout/navTrail.js +19 -0
  60. package/dist/esm/v1/components/layout/navTrail.js.map +1 -0
  61. package/dist/esm/v1/components/overlays/ConfirmDialog/ConfirmDialog.js +95 -0
  62. package/dist/esm/v1/components/overlays/ConfirmDialog/ConfirmDialog.js.map +1 -0
  63. package/dist/esm/v1/components/overlays/Dialog/Dialog.js.map +1 -1
  64. package/dist/esm/v1/components/overlays/Tooltip/Tooltip.js +98 -0
  65. package/dist/esm/v1/components/overlays/Tooltip/Tooltip.js.map +1 -0
  66. package/dist/esm/v1/components/overlays/Tooltip/TooltipLayer.js +84 -0
  67. package/dist/esm/v1/components/overlays/Tooltip/TooltipLayer.js.map +1 -0
  68. package/dist/esm/v1/components/overlays/Tooltip/TooltipPanel.js +33 -0
  69. package/dist/esm/v1/components/overlays/Tooltip/TooltipPanel.js.map +1 -0
  70. package/dist/esm/v1/components/overlays/topLayer.js +28 -0
  71. package/dist/esm/v1/components/overlays/topLayer.js.map +1 -0
  72. package/dist/esm/v1/foundations/icons/icons.js +61 -2
  73. package/dist/esm/v1/foundations/icons/icons.js.map +1 -1
  74. package/dist/esm/v1/primitives/buttons/Button/Button.js +18 -4
  75. package/dist/esm/v1/primitives/buttons/Button/Button.js.map +1 -1
  76. package/dist/esm/v1/primitives/buttons/SegmentedControl/SegmentedControl.js +65 -0
  77. package/dist/esm/v1/primitives/buttons/SegmentedControl/SegmentedControl.js.map +1 -0
  78. package/dist/esm/v1/primitives/buttons/SegmentedControl/segmentedStyles.js +10 -0
  79. package/dist/esm/v1/primitives/buttons/SegmentedControl/segmentedStyles.js.map +1 -0
  80. package/dist/esm/v1/primitives/display/Avatar/Avatar.js +120 -0
  81. package/dist/esm/v1/primitives/display/Avatar/Avatar.js.map +1 -0
  82. package/dist/esm/v1/primitives/display/Badge/Badge.js +23 -0
  83. package/dist/esm/v1/primitives/display/Badge/Badge.js.map +1 -0
  84. package/dist/esm/v1/primitives/display/Card/Card.js +22 -0
  85. package/dist/esm/v1/primitives/display/Card/Card.js.map +1 -0
  86. package/dist/esm/v1/primitives/display/FilterChips/FilterChips.js +45 -0
  87. package/dist/esm/v1/primitives/display/FilterChips/FilterChips.js.map +1 -0
  88. package/dist/esm/v1/primitives/display/Kpi/Kpi.js +103 -0
  89. package/dist/esm/v1/primitives/display/Kpi/Kpi.js.map +1 -0
  90. package/dist/esm/v1/primitives/display/Notice/Notice.js +71 -0
  91. package/dist/esm/v1/primitives/display/Notice/Notice.js.map +1 -0
  92. package/dist/esm/v1/primitives/display/Person/Person.js +14 -0
  93. package/dist/esm/v1/primitives/display/Person/Person.js.map +1 -0
  94. package/dist/esm/v1/primitives/forms/Checkbox/Checkbox.js +1 -1
  95. package/dist/esm/v1/primitives/forms/Checkbox/Checkbox.js.map +1 -1
  96. package/dist/esm/v1/primitives/forms/DateInput/DateInput.js +150 -0
  97. package/dist/esm/v1/primitives/forms/DateInput/DateInput.js.map +1 -0
  98. package/dist/esm/v1/primitives/forms/SearchInput/SearchInput.js +43 -0
  99. package/dist/esm/v1/primitives/forms/SearchInput/SearchInput.js.map +1 -0
  100. package/dist/esm/v1/primitives/forms/SelectInput/SelectInput.js +190 -12
  101. package/dist/esm/v1/primitives/forms/SelectInput/SelectInput.js.map +1 -1
  102. package/dist/esm/v1/primitives/forms/field.js +4 -2
  103. package/dist/esm/v1/primitives/forms/field.js.map +1 -1
  104. package/dist/esm/v1/primitives/navigation/Tabs/Tabs.js +96 -0
  105. package/dist/esm/v1/primitives/navigation/Tabs/Tabs.js.map +1 -0
  106. package/dist/style.components.css +1 -1
  107. package/dist/style.css +1 -1
  108. package/dist/style.layered.css +1 -1
  109. package/dist/tailwind.css +24 -0
  110. package/dist/theme-boot.js +1 -0
  111. package/dist/theme.css +10 -3
  112. package/dist/tokens.css +119 -0
  113. package/dist/types/legacy/components/atoms/InputData/InputData.d.ts +4 -1
  114. package/dist/types/legacy/components/charts/Charts.d.ts +58 -5
  115. package/dist/types/legacy/components/layout/Layout/Layout.d.ts +7 -20
  116. package/dist/types/legacy/components/layout/Sidebar/Sidebar.d.ts +13 -26
  117. package/dist/types/legacy/components/molecules/ColumnPicker/ColumnPicker.d.ts +56 -3
  118. package/dist/types/legacy/components/molecules/DataTable/DataTable.d.ts +72 -7
  119. package/dist/types/legacy/components/molecules/UserAvatar/UserAvatar.d.ts +4 -5
  120. package/dist/types/legacy/components/ui/Avisos.d.ts +6 -16
  121. package/dist/types/legacy/components/ui/CabecalhoPagina.d.ts +6 -20
  122. package/dist/types/legacy/components/ui/CaixaPesquisa.d.ts +2 -4
  123. package/dist/types/legacy/components/ui/Confirmacao.d.ts +3 -10
  124. package/dist/types/legacy/components/ui/Escolha.d.ts +5 -15
  125. package/dist/types/legacy/components/ui/FaixaAviso.d.ts +2 -6
  126. package/dist/types/legacy/components/ui/Separadores.d.ts +3 -10
  127. package/dist/types/legacy/components/ui/Ui.d.ts +12 -54
  128. package/dist/types/legacy/components/ui/painelFlutuante.d.ts +6 -10
  129. package/dist/types/legacy/exports/atoms.d.ts +1 -1
  130. package/dist/types/legacy/exports/charts.d.ts +2 -2
  131. package/dist/types/legacy/exports/hooks.d.ts +2 -2
  132. package/dist/types/legacy/exports/molecules.d.ts +2 -2
  133. package/dist/types/v1/capabilities/overlay/useFloatingPanel.d.ts +20 -0
  134. package/dist/types/v1/capabilities/theme/useIsDark.d.ts +10 -0
  135. package/dist/types/v1/capabilities/theme/useTheme.d.ts +29 -0
  136. package/dist/types/v1/components/data/Pagination/Pagination.d.ts +33 -0
  137. package/dist/types/v1/components/feedback/Toast/Toast.d.ts +51 -0
  138. package/dist/types/v1/components/layout/AppShell/AppShell.d.ts +35 -0
  139. package/dist/types/v1/components/layout/Page/Page.d.ts +23 -0
  140. package/dist/types/v1/components/layout/PageHeader/PageHeader.d.ts +35 -0
  141. package/dist/types/v1/components/layout/SideNav/SideNav.d.ts +92 -0
  142. package/dist/types/v1/components/layout/navTrail.d.ts +15 -0
  143. package/dist/types/v1/components/overlays/ConfirmDialog/ConfirmDialog.d.ts +59 -0
  144. package/dist/types/v1/components/overlays/Dialog/Dialog.d.ts +3 -2
  145. package/dist/types/v1/components/overlays/Tooltip/Tooltip.d.ts +34 -0
  146. package/dist/types/v1/components/overlays/Tooltip/TooltipLayer.d.ts +18 -0
  147. package/dist/types/v1/components/overlays/Tooltip/TooltipPanel.d.ts +23 -0
  148. package/dist/types/v1/components/overlays/topLayer.d.ts +23 -0
  149. package/dist/types/v1/foundations/icons/icons.d.ts +20 -0
  150. package/dist/types/v1/index.d.ts +43 -1
  151. package/dist/types/v1/primitives/buttons/Button/Button.d.ts +20 -5
  152. package/dist/types/v1/primitives/buttons/SegmentedControl/SegmentedControl.d.ts +28 -0
  153. package/dist/types/v1/primitives/buttons/SegmentedControl/segmentedStyles.d.ts +16 -0
  154. package/dist/types/v1/primitives/display/Avatar/Avatar.d.ts +30 -0
  155. package/dist/types/v1/primitives/display/Badge/Badge.d.ts +25 -0
  156. package/dist/types/v1/primitives/display/Card/Card.d.ts +22 -0
  157. package/dist/types/v1/primitives/display/FilterChips/FilterChips.d.ts +33 -0
  158. package/dist/types/v1/primitives/display/Kpi/Kpi.d.ts +68 -0
  159. package/dist/types/v1/primitives/display/Notice/Notice.d.ts +29 -0
  160. package/dist/types/v1/primitives/display/Person/Person.d.ts +15 -0
  161. package/dist/types/v1/primitives/forms/DateInput/DateInput.d.ts +43 -0
  162. package/dist/types/v1/primitives/forms/SearchInput/SearchInput.d.ts +23 -0
  163. package/dist/types/v1/primitives/forms/SelectInput/SelectInput.d.ts +60 -12
  164. package/dist/types/v1/primitives/forms/field.d.ts +2 -0
  165. package/dist/types/v1/primitives/navigation/Tabs/Tabs.d.ts +58 -0
  166. package/package.json +3 -3
  167. package/dist/esm/legacy/components/atoms/Tooltip/Tooltip.css +0 -17
  168. package/dist/esm/legacy/components/atoms/Tooltip/Tooltip.js +0 -41
  169. package/dist/esm/legacy/components/atoms/Tooltip/Tooltip.js.map +0 -1
  170. package/dist/esm/legacy/components/molecules/Search/Search.css +0 -18
  171. package/dist/esm/legacy/components/molecules/Search/Search.js +0 -30
  172. package/dist/esm/legacy/components/molecules/Search/Search.js.map +0 -1
  173. package/dist/esm/legacy/hooks/useTheme.js +0 -39
  174. package/dist/esm/legacy/hooks/useTheme.js.map +0 -1
  175. package/dist/types/legacy/components/atoms/Tooltip/Tooltip.d.ts +0 -13
  176. package/dist/types/legacy/components/molecules/Search/Search.d.ts +0 -7
  177. package/dist/types/legacy/hooks/useTheme.d.ts +0 -17
@@ -0,0 +1,89 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import { useMemo, useState, useEffect, useRef, useLayoutEffect } from "react";
3
+ import { useLocation } from "react-router-dom";
4
+ import { MenuIcon } from "../../../foundations/icons/icons.js";
5
+ import SideNav, { isDrawer } from "../SideNav/SideNav.js";
6
+ import { findNavTrail, NavTrailContext } from "../navTrail.js";
7
+ function AppShell({
8
+ children,
9
+ sidebarFooter,
10
+ banners,
11
+ topBar,
12
+ fullHeight = false,
13
+ defaultOpen = true,
14
+ onOpenChange,
15
+ labels,
16
+ ...sideNav
17
+ }) {
18
+ const location = useLocation();
19
+ const { menu, groupLabels } = sideNav;
20
+ const trail = useMemo(() => findNavTrail(menu, groupLabels ?? {}, location.pathname), [menu, groupLabels, location.pathname]);
21
+ const [open, setOpenState] = useState(() => !isDrawer() && defaultOpen);
22
+ const setOpen = (value) => {
23
+ setOpenState(value);
24
+ onOpenChange?.(value);
25
+ };
26
+ useEffect(() => {
27
+ const onResize = () => {
28
+ if (isDrawer()) setOpenState(false);
29
+ };
30
+ window.addEventListener("resize", onResize);
31
+ return () => window.removeEventListener("resize", onResize);
32
+ }, []);
33
+ useEffect(() => {
34
+ if (isDrawer()) setOpenState(false);
35
+ }, [location.pathname]);
36
+ const bannersRef = useRef(null);
37
+ const [top, setTop] = useState(0);
38
+ useLayoutEffect(() => {
39
+ const el = bannersRef.current;
40
+ if (!el) {
41
+ setTop(0);
42
+ return;
43
+ }
44
+ const measure = () => setTop(el.offsetHeight);
45
+ measure();
46
+ if (typeof ResizeObserver === "undefined") return;
47
+ const ro = new ResizeObserver(measure);
48
+ ro.observe(el);
49
+ return () => ro.disconnect();
50
+ }, [banners]);
51
+ const margin = open ? "lg:ml-64" : "lg:ml-20";
52
+ return /* @__PURE__ */ jsxs(
53
+ "div",
54
+ {
55
+ style: { "--app-shell-top": `${top}px` },
56
+ className: `flex bg-background text-foreground ${fullHeight ? "h-dvh overflow-hidden" : "min-h-dvh"}`,
57
+ children: [
58
+ banners && /* @__PURE__ */ jsx("div", { ref: bannersRef, className: "fixed inset-x-0 top-0 z-50", children: banners }),
59
+ /* @__PURE__ */ jsx(SideNav, { ...sideNav, footer: sidebarFooter, labels, open, onOpenChange: setOpen }),
60
+ open && /* @__PURE__ */ jsx("div", { onClick: () => setOpen(false), "aria-hidden": true, className: "fixed inset-0 z-30 bg-slate-900/50 backdrop-blur-sm lg:hidden" }),
61
+ /* @__PURE__ */ jsxs("div", { className: "flex min-w-0 flex-1 flex-col", style: { paddingTop: "var(--app-shell-top)" }, children: [
62
+ !open && /* @__PURE__ */ jsx(
63
+ "button",
64
+ {
65
+ type: "button",
66
+ onClick: () => setOpen(true),
67
+ "aria-label": labels?.openMenu ?? "Abrir menu",
68
+ style: { top: "calc(var(--app-shell-top) + 1.25rem)" },
69
+ className: "fixed left-5 z-40 flex cursor-pointer items-center justify-center rounded-xl border border-border bg-card p-3 text-muted-foreground shadow-page transition-colors hover:text-proef-blue lg:hidden",
70
+ children: /* @__PURE__ */ jsx(MenuIcon, { size: 20 })
71
+ }
72
+ ),
73
+ topBar && /* @__PURE__ */ jsx("div", { style: { top: "var(--app-shell-top)" }, className: `sticky z-30 transition-[margin] duration-300 ${margin}`, children: topBar }),
74
+ /* @__PURE__ */ jsx(
75
+ "main",
76
+ {
77
+ className: `min-w-0 px-page-x-narrow transition-[margin] duration-300 sm:px-page-x ${margin} ${topBar ? "pt-6" : "pt-page-top-narrow lg:pt-page-top"} ` + (fullHeight ? "flex min-h-0 flex-1 flex-col overflow-hidden pb-4" : "pb-16"),
78
+ children: /* @__PURE__ */ jsx(NavTrailContext.Provider, { value: trail, children })
79
+ }
80
+ )
81
+ ] })
82
+ ]
83
+ }
84
+ );
85
+ }
86
+ export {
87
+ AppShell as default
88
+ };
89
+ //# sourceMappingURL=AppShell.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"AppShell.js","sources":["../../../../../../src/v1/components/layout/AppShell/AppShell.tsx"],"sourcesContent":["import React, { useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react'\r\nimport { useLocation } from 'react-router-dom'\r\nimport { MenuIcon } from '../../../foundations/icons/icons'\r\nimport SideNav, { isDrawer, type SideNavLabels, type SideNavProps } from '../SideNav/SideNav'\r\nimport { NavTrailContext, findNavTrail } from '../navTrail'\r\n\r\nexport interface AppShellLabels extends SideNavLabels {\r\n openMenu: string\r\n}\r\n\r\nexport interface AppShellProps extends Omit<SideNavProps, 'open' | 'onOpenChange' | 'footer' | 'labels'> {\r\n children: React.ReactNode\r\n /** Conteúdo extra no rodapé do menu, por cima do tema (ex: um filtro global). */\r\n sidebarFooter?: (expanded: boolean) => React.ReactNode\r\n /** Barras no topo, com a largura toda (ex: \"A ver como…\", \"A sincronizar…\"). Empurram o menu e a página para baixo. */\r\n banners?: React.ReactNode\r\n /** Barra fixa por cima da página, à direita do menu (ex: título e alertas). Traz o seu próprio fundo. */\r\n topBar?: React.ReactNode\r\n /**\r\n * A página ocupa a altura que sobra do ecrã, sem scroll da página (ex:\r\n * quadros e listas que fazem scroll por dentro, com o `Page` em `fill`).\r\n * As margens são as mesmas das outras páginas.\r\n */\r\n fullHeight?: boolean\r\n /** Menu aberto ao entrar (ecrãs grandes). Por omissão sim; fechado fica só com os ícones. */\r\n defaultOpen?: boolean\r\n /** A pessoa abriu ou fechou o menu (para a app guardar a escolha, se quiser). */\r\n onOpenChange?: (open: boolean) => void\r\n labels?: Partial<AppShellLabels>\r\n}\r\n\r\n/**\r\n * A moldura da app: o menu lateral (`SideNav`) à esquerda e a página à\r\n * direita, com barras opcionais no topo.\r\n *\r\n * Em ecrãs pequenos (< 1024px) o menu é uma gaveta, aberta pelo botão no canto,\r\n * que fecha ao mudar de página.\r\n *\r\n * Precisa de estar dentro de um Router do react-router.\r\n */\r\nexport default function AppShell({\r\n children,\r\n sidebarFooter,\r\n banners,\r\n topBar,\r\n fullHeight = false,\r\n defaultOpen = true,\r\n onOpenChange,\r\n labels,\r\n ...sideNav\r\n}: AppShellProps) {\r\n const location = useLocation()\r\n // O lugar da página no menu, para o `Page` mostrar o caminho \"grupo › página\".\r\n const { menu, groupLabels } = sideNav\r\n const trail = useMemo(() => findNavTrail(menu, groupLabels ?? {}, location.pathname), [menu, groupLabels, location.pathname])\r\n const [open, setOpenState] = useState(() => !isDrawer() && defaultOpen)\r\n\r\n // Escolha da pessoa: avisa a app. O ajuste ao redimensionar não é escolha dela.\r\n const setOpen = (value: boolean) => {\r\n setOpenState(value)\r\n onOpenChange?.(value)\r\n }\r\n\r\n useEffect(() => {\r\n const onResize = () => {\r\n if (isDrawer()) setOpenState(false)\r\n }\r\n window.addEventListener('resize', onResize)\r\n return () => window.removeEventListener('resize', onResize)\r\n }, [])\r\n\r\n // A gaveta fecha ao mudar de página (incluindo links fora do menu).\r\n useEffect(() => {\r\n if (isDrawer()) setOpenState(false)\r\n }, [location.pathname])\r\n\r\n // Altura das barras do topo: o menu e a página começam por baixo delas.\r\n const bannersRef = useRef<HTMLDivElement>(null)\r\n const [top, setTop] = useState(0)\r\n useLayoutEffect(() => {\r\n const el = bannersRef.current\r\n if (!el) {\r\n setTop(0)\r\n return\r\n }\r\n const measure = () => setTop(el.offsetHeight)\r\n measure()\r\n if (typeof ResizeObserver === 'undefined') return\r\n const ro = new ResizeObserver(measure)\r\n ro.observe(el)\r\n return () => ro.disconnect()\r\n }, [banners])\r\n\r\n const margin = open ? 'lg:ml-64' : 'lg:ml-20'\r\n\r\n return (\r\n <div\r\n style={{ '--app-shell-top': `${top}px` } as React.CSSProperties}\r\n className={`flex bg-background text-foreground ${fullHeight ? 'h-dvh overflow-hidden' : 'min-h-dvh'}`}\r\n >\r\n {banners && (\r\n <div ref={bannersRef} className=\"fixed inset-x-0 top-0 z-50\">\r\n {banners}\r\n </div>\r\n )}\r\n\r\n <SideNav {...sideNav} footer={sidebarFooter} labels={labels} open={open} onOpenChange={setOpen} />\r\n\r\n {open && <div onClick={() => setOpen(false)} aria-hidden className=\"fixed inset-0 z-30 bg-slate-900/50 backdrop-blur-sm lg:hidden\" />}\r\n\r\n <div className=\"flex min-w-0 flex-1 flex-col\" style={{ paddingTop: 'var(--app-shell-top)' }}>\r\n {!open && (\r\n <button\r\n type=\"button\"\r\n onClick={() => setOpen(true)}\r\n aria-label={labels?.openMenu ?? 'Abrir menu'}\r\n style={{ top: 'calc(var(--app-shell-top) + 1.25rem)' }}\r\n className=\"fixed left-5 z-40 flex cursor-pointer items-center justify-center rounded-xl border border-border bg-card p-3 text-muted-foreground shadow-page transition-colors hover:text-proef-blue lg:hidden\"\r\n >\r\n <MenuIcon size={20} />\r\n </button>\r\n )}\r\n {topBar && (\r\n <div style={{ top: 'var(--app-shell-top)' }} className={`sticky z-30 transition-[margin] duration-300 ${margin}`}>\r\n {topBar}\r\n </div>\r\n )}\r\n {/* As margens da página vêm daqui, iguais em todas as páginas (também nas\r\n de altura total) e com o menu aberto ou fechado: as páginas não põem as\r\n suas. Os valores são tokens (--spacing-page-x, --spacing-page-top). Em\r\n ecrãs pequenos o espaço de cima deixa lugar ao botão do menu. */}\r\n <main\r\n className={\r\n `min-w-0 px-page-x-narrow transition-[margin] duration-300 sm:px-page-x ${margin} ${topBar ? 'pt-6' : 'pt-page-top-narrow lg:pt-page-top'} ` +\r\n (fullHeight ? 'flex min-h-0 flex-1 flex-col overflow-hidden pb-4' : 'pb-16')\r\n }\r\n >\r\n <NavTrailContext.Provider value={trail}>{children}</NavTrailContext.Provider>\r\n </main>\r\n </div>\r\n </div>\r\n )\r\n}\r\n"],"names":[],"mappings":";;;;;;AAwCA,SAAwB,SAAS;AAAA,EAC/B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,aAAa;AAAA,EACb,cAAc;AAAA,EACd;AAAA,EACA;AAAA,EACA,GAAG;AACL,GAAkB;AAChB,QAAM,WAAW,YAAA;AAEjB,QAAM,EAAE,MAAM,YAAA,IAAgB;AAC9B,QAAM,QAAQ,QAAQ,MAAM,aAAa,MAAM,eAAe,CAAA,GAAI,SAAS,QAAQ,GAAG,CAAC,MAAM,aAAa,SAAS,QAAQ,CAAC;AAC5H,QAAM,CAAC,MAAM,YAAY,IAAI,SAAS,MAAM,CAAC,SAAA,KAAc,WAAW;AAGtE,QAAM,UAAU,CAAC,UAAmB;AAClC,iBAAa,KAAK;AAClB,mBAAe,KAAK;AAAA,EACtB;AAEA,YAAU,MAAM;AACd,UAAM,WAAW,MAAM;AACrB,UAAI,SAAA,EAAY,cAAa,KAAK;AAAA,IACpC;AACA,WAAO,iBAAiB,UAAU,QAAQ;AAC1C,WAAO,MAAM,OAAO,oBAAoB,UAAU,QAAQ;AAAA,EAC5D,GAAG,CAAA,CAAE;AAGL,YAAU,MAAM;AACd,QAAI,SAAA,EAAY,cAAa,KAAK;AAAA,EACpC,GAAG,CAAC,SAAS,QAAQ,CAAC;AAGtB,QAAM,aAAa,OAAuB,IAAI;AAC9C,QAAM,CAAC,KAAK,MAAM,IAAI,SAAS,CAAC;AAChC,kBAAgB,MAAM;AACpB,UAAM,KAAK,WAAW;AACtB,QAAI,CAAC,IAAI;AACP,aAAO,CAAC;AACR;AAAA,IACF;AACA,UAAM,UAAU,MAAM,OAAO,GAAG,YAAY;AAC5C,YAAA;AACA,QAAI,OAAO,mBAAmB,YAAa;AAC3C,UAAM,KAAK,IAAI,eAAe,OAAO;AACrC,OAAG,QAAQ,EAAE;AACb,WAAO,MAAM,GAAG,WAAA;AAAA,EAClB,GAAG,CAAC,OAAO,CAAC;AAEZ,QAAM,SAAS,OAAO,aAAa;AAEnC,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,OAAO,EAAE,mBAAmB,GAAG,GAAG,KAAA;AAAA,MAClC,WAAW,sCAAsC,aAAa,0BAA0B,WAAW;AAAA,MAElG,UAAA;AAAA,QAAA,+BACE,OAAA,EAAI,KAAK,YAAY,WAAU,8BAC7B,UAAA,SACH;AAAA,QAGF,oBAAC,WAAS,GAAG,SAAS,QAAQ,eAAe,QAAgB,MAAY,cAAc,SAAS;AAAA,QAE/F,QAAQ,oBAAC,OAAA,EAAI,SAAS,MAAM,QAAQ,KAAK,GAAG,eAAW,MAAC,WAAU,gEAAA,CAAgE;AAAA,QAEnI,qBAAC,SAAI,WAAU,gCAA+B,OAAO,EAAE,YAAY,0BAChE,UAAA;AAAA,UAAA,CAAC,QACA;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,MAAK;AAAA,cACL,SAAS,MAAM,QAAQ,IAAI;AAAA,cAC3B,cAAY,QAAQ,YAAY;AAAA,cAChC,OAAO,EAAE,KAAK,uCAAA;AAAA,cACd,WAAU;AAAA,cAEV,UAAA,oBAAC,UAAA,EAAS,MAAM,GAAA,CAAI;AAAA,YAAA;AAAA,UAAA;AAAA,UAGvB,UACC,oBAAC,OAAA,EAAI,OAAO,EAAE,KAAK,uBAAA,GAA0B,WAAW,gDAAgD,MAAM,IAC3G,UAAA,OAAA,CACH;AAAA,UAMF;AAAA,YAAC;AAAA,YAAA;AAAA,cACC,WACE,0EAA0E,MAAM,IAAI,SAAS,SAAS,mCAAmC,OACxI,aAAa,sDAAsD;AAAA,cAGtE,8BAAC,gBAAgB,UAAhB,EAAyB,OAAO,OAAQ,SAAA,CAAS;AAAA,YAAA;AAAA,UAAA;AAAA,QACpD,EAAA,CACF;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGN;"}
@@ -0,0 +1,16 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ import PageHeader from "../PageHeader/PageHeader.js";
3
+ import { useNavTrail } from "../navTrail.js";
4
+ function Page({ tabs, fill = false, children, breadcrumb, ...header }) {
5
+ const trail = useNavTrail();
6
+ const auto = trail?.group ? [{ label: trail.group }, { label: trail.page }] : void 0;
7
+ return /* @__PURE__ */ jsxs("div", { className: `flex flex-col gap-4 ${fill ? "min-h-0 flex-1" : ""}`, children: [
8
+ /* @__PURE__ */ jsx(PageHeader, { ...header, breadcrumb: breadcrumb ?? auto }),
9
+ tabs && /* @__PURE__ */ jsx("div", { className: "flex-none", children: tabs }),
10
+ /* @__PURE__ */ jsx("div", { className: `${tabs ? "pt-2" : "pt-4"} ${fill ? "flex min-h-0 flex-1 flex-col overflow-hidden" : ""}`, children })
11
+ ] });
12
+ }
13
+ export {
14
+ Page as default
15
+ };
16
+ //# sourceMappingURL=Page.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"Page.js","sources":["../../../../../../src/v1/components/layout/Page/Page.tsx"],"sourcesContent":["import React from 'react'\r\nimport PageHeader, { type PageHeaderProps } from '../PageHeader/PageHeader'\r\nimport { useNavTrail } from '../navTrail'\r\n\r\nexport interface PageProps extends PageHeaderProps {\r\n /** Separadores da página (ex: `<Tabs …/>`), entre o cabeçalho e o conteúdo. */\r\n tabs?: React.ReactNode\r\n /**\r\n * A página ocupa a altura que sobra e o conteúdo faz scroll por dentro (ex:\r\n * quadros e listas compridas). Usar com o `fullHeight` do `AppShell`.\r\n */\r\n fill?: boolean\r\n children: React.ReactNode\r\n}\r\n\r\n/**\r\n * Uma página da app: cabeçalho (`PageHeader`), separadores opcionais e o\r\n * conteúdo, sempre com os mesmos espaços entre eles. As margens à volta vêm do\r\n * `AppShell`; a página não põe as suas.\r\n *\r\n * **Caminho automático**: dentro do `AppShell`, numa página que está num grupo\r\n * do menu, o caminho por cima do título é \"grupo › página\" (ex: \"App & Dev ›\r\n * Sprints\"), com os nomes do menu. Passar `breadcrumb` substitui-o (ex: no\r\n * detalhe de um registo); `breadcrumb={[]}` tira-o.\r\n */\r\nexport default function Page({ tabs, fill = false, children, breadcrumb, ...header }: PageProps) {\r\n const trail = useNavTrail()\r\n const auto = trail?.group ? [{ label: trail.group }, { label: trail.page }] : undefined\r\n\r\n return (\r\n <div className={`flex flex-col gap-4 ${fill ? 'min-h-0 flex-1' : ''}`}>\r\n <PageHeader {...header} breadcrumb={breadcrumb ?? auto} />\r\n {tabs && <div className=\"flex-none\">{tabs}</div>}\r\n <div className={`${tabs ? 'pt-2' : 'pt-4'} ${fill ? 'flex min-h-0 flex-1 flex-col overflow-hidden' : ''}`}>{children}</div>\r\n </div>\r\n )\r\n}\r\n"],"names":[],"mappings":";;;AAyBA,SAAwB,KAAK,EAAE,MAAM,OAAO,OAAO,UAAU,YAAY,GAAG,UAAqB;AAC/F,QAAM,QAAQ,YAAA;AACd,QAAM,OAAO,OAAO,QAAQ,CAAC,EAAE,OAAO,MAAM,MAAA,GAAS,EAAE,OAAO,MAAM,KAAA,CAAM,IAAI;AAE9E,8BACG,OAAA,EAAI,WAAW,uBAAuB,OAAO,mBAAmB,EAAE,IACjE,UAAA;AAAA,IAAA,oBAAC,YAAA,EAAY,GAAG,QAAQ,YAAY,cAAc,MAAM;AAAA,IACvD,QAAQ,oBAAC,OAAA,EAAI,WAAU,aAAa,UAAA,MAAK;AAAA,IAC1C,oBAAC,OAAA,EAAI,WAAW,GAAG,OAAO,SAAS,MAAM,IAAI,OAAO,iDAAiD,EAAE,IAAK,SAAA,CAAS;AAAA,EAAA,GACvH;AAEJ;"}
@@ -0,0 +1,26 @@
1
+ import { jsxs, jsx } from "react/jsx-runtime";
2
+ function PageHeader({ title, description, breadcrumb, actions, extra, breadcrumbLabel = "Caminho" }) {
3
+ return /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-3", children: [
4
+ breadcrumb && breadcrumb.length > 0 && /* @__PURE__ */ jsx("nav", { "aria-label": breadcrumbLabel, children: /* @__PURE__ */ jsx("ol", { className: "m-0 flex list-none flex-wrap items-center gap-1.5 p-0 text-xs font-semibold text-muted-foreground", children: breadcrumb.map((n, i) => {
5
+ const last = i === breadcrumb.length - 1;
6
+ const link = "cursor-pointer border-0 bg-transparent p-0 font-semibold text-proef-blue no-underline hover:text-primary hover:underline dark:text-sky-300 dark:hover:text-sky-200";
7
+ return /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-1.5", children: [
8
+ last ? /* @__PURE__ */ jsx("span", { "aria-current": "page", className: "text-foreground/70", children: n.label }) : n.href ? /* @__PURE__ */ jsx("a", { href: n.href, onClick: n.onClick, className: link, children: n.label }) : n.onClick ? /* @__PURE__ */ jsx("button", { type: "button", onClick: n.onClick, className: `text-xs ${link}`, children: n.label }) : /* @__PURE__ */ jsx("span", { children: n.label }),
9
+ !last && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-muted-foreground/60", children: "›" })
10
+ ] }, i);
11
+ }) }) }),
12
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between", children: [
13
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
14
+ /* @__PURE__ */ jsx("h1", { className: "m-0 text-page-title font-black tracking-tighter text-primary uppercase dark:text-foreground", children: title }),
15
+ /* @__PURE__ */ jsx("div", { className: "mt-1 h-1 w-12 bg-proef-turquoise" }),
16
+ description && /* @__PURE__ */ jsx("p", { className: "m-0 mt-3 text-sm font-medium text-muted-foreground", children: description }),
17
+ extra && /* @__PURE__ */ jsx("div", { className: "mt-2 flex flex-wrap items-center gap-2 empty:hidden", children: extra })
18
+ ] }),
19
+ actions && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 flex-wrap items-center gap-2 empty:hidden", children: actions })
20
+ ] })
21
+ ] });
22
+ }
23
+ export {
24
+ PageHeader as default
25
+ };
26
+ //# sourceMappingURL=PageHeader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"PageHeader.js","sources":["../../../../../../src/v1/components/layout/PageHeader/PageHeader.tsx"],"sourcesContent":["import React from 'react'\r\n\r\n/** Um nível do caminho por cima do título. */\r\nexport interface BreadcrumbItem {\r\n label: React.ReactNode\r\n /** Ligação normal. Para o router da app, usar `onClick` (ex: `() => navigate('/x')`). */\r\n href?: string\r\n onClick?: () => void\r\n}\r\n\r\nexport interface PageHeaderProps {\r\n title: React.ReactNode\r\n description?: React.ReactNode\r\n /**\r\n * Caminho até à página (área › lista › registo). O último nível é a página\r\n * atual e não leva ligação, mesmo que a traga.\r\n */\r\n breadcrumb?: BreadcrumbItem[]\r\n /** Botões da página (ex: \"Novo\", \"Editar\"). Ficam à direita e descem em ecrã estreito. */\r\n actions?: React.ReactNode\r\n /** Linha extra por baixo da descrição (etiquetas de estado, responsável…). */\r\n extra?: React.ReactNode\r\n /** Nome da navegação do caminho, para leitores de ecrã. Por omissão: \"Caminho\". */\r\n breadcrumbLabel?: string\r\n}\r\n\r\n/**\r\n * Cabeçalho comum a todas as páginas: caminho, título com a barra turquesa,\r\n * descrição e ações.\r\n *\r\n * Existe para o título deixar de ser desenhado à mão em cada ecrã — com\r\n * tamanhos, cores e posições diferentes de página para página. O nome no menu,\r\n * o título e o último nível do caminho devem ser o mesmo texto.\r\n *\r\n * Normalmente usa-se através do `Page`, que também arruma os separadores e o\r\n * conteúdo por baixo.\r\n */\r\nexport default function PageHeader({ title, description, breadcrumb, actions, extra, breadcrumbLabel = 'Caminho' }: PageHeaderProps) {\r\n return (\r\n <header className=\"flex flex-col gap-3\">\r\n {breadcrumb && breadcrumb.length > 0 && (\r\n <nav aria-label={breadcrumbLabel}>\r\n <ol className=\"m-0 flex list-none flex-wrap items-center gap-1.5 p-0 text-xs font-semibold text-muted-foreground\">\r\n {breadcrumb.map((n, i) => {\r\n const last = i === breadcrumb.length - 1\r\n const link = 'cursor-pointer border-0 bg-transparent p-0 font-semibold text-proef-blue no-underline hover:text-primary hover:underline dark:text-sky-300 dark:hover:text-sky-200'\r\n return (\r\n <li key={i} className=\"flex items-center gap-1.5\">\r\n {last ? (\r\n <span aria-current=\"page\" className=\"text-foreground/70\">\r\n {n.label}\r\n </span>\r\n ) : n.href ? (\r\n <a href={n.href} onClick={n.onClick} className={link}>\r\n {n.label}\r\n </a>\r\n ) : n.onClick ? (\r\n <button type=\"button\" onClick={n.onClick} className={`text-xs ${link}`}>\r\n {n.label}\r\n </button>\r\n ) : (\r\n <span>{n.label}</span>\r\n )}\r\n {!last && (\r\n <span aria-hidden=\"true\" className=\"text-muted-foreground/60\">\r\n ›\r\n </span>\r\n )}\r\n </li>\r\n )\r\n })}\r\n </ol>\r\n </nav>\r\n )}\r\n <div className=\"flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between\">\r\n <div className=\"min-w-0\">\r\n {/* Tamanho pelo token --text-page-title (tokens.css): igual em todos os portais e ecrãs. */}\r\n <h1 className=\"m-0 text-page-title font-black tracking-tighter text-primary uppercase dark:text-foreground\">{title}</h1>\r\n <div className=\"mt-1 h-1 w-12 bg-proef-turquoise\" />\r\n {description && <p className=\"m-0 mt-3 text-sm font-medium text-muted-foreground\">{description}</p>}\r\n {/* empty:hidden: um botão condicional que não aparece (ex: `<>{cond && <b/>}</>`)\r\n não deixa espaço vazio por baixo do título. */}\r\n {extra && <div className=\"mt-2 flex flex-wrap items-center gap-2 empty:hidden\">{extra}</div>}\r\n </div>\r\n {actions && <div className=\"flex shrink-0 flex-wrap items-center gap-2 empty:hidden\">{actions}</div>}\r\n </div>\r\n </header>\r\n )\r\n}\r\n"],"names":[],"mappings":";AAqCA,SAAwB,WAAW,EAAE,OAAO,aAAa,YAAY,SAAS,OAAO,kBAAkB,aAA8B;AACnI,SACE,qBAAC,UAAA,EAAO,WAAU,uBACf,UAAA;AAAA,IAAA,cAAc,WAAW,SAAS,KACjC,oBAAC,SAAI,cAAY,iBACf,UAAA,oBAAC,MAAA,EAAG,WAAU,qGACX,UAAA,WAAW,IAAI,CAAC,GAAG,MAAM;AACxB,YAAM,OAAO,MAAM,WAAW,SAAS;AACvC,YAAM,OAAO;AACb,aACE,qBAAC,MAAA,EAAW,WAAU,6BACnB,UAAA;AAAA,QAAA,2BACE,QAAA,EAAK,gBAAa,QAAO,WAAU,sBACjC,YAAE,MAAA,CACL,IACE,EAAE,OACJ,oBAAC,OAAE,MAAM,EAAE,MAAM,SAAS,EAAE,SAAS,WAAW,MAC7C,YAAE,MAAA,CACL,IACE,EAAE,UACJ,oBAAC,YAAO,MAAK,UAAS,SAAS,EAAE,SAAS,WAAW,WAAW,IAAI,IACjE,UAAA,EAAE,MAAA,CACL,IAEA,oBAAC,QAAA,EAAM,YAAE,MAAA,CAAM;AAAA,QAEhB,CAAC,QACA,oBAAC,QAAA,EAAK,eAAY,QAAO,WAAU,4BAA2B,UAAA,IAAA,CAE9D;AAAA,MAAA,EAAA,GAnBK,CAqBT;AAAA,IAEJ,CAAC,GACH,GACF;AAAA,IAEF,qBAAC,OAAA,EAAI,WAAU,mEACb,UAAA;AAAA,MAAA,qBAAC,OAAA,EAAI,WAAU,WAEb,UAAA;AAAA,QAAA,oBAAC,MAAA,EAAG,WAAU,+FAA+F,UAAA,OAAM;AAAA,QACnH,oBAAC,OAAA,EAAI,WAAU,mCAAA,CAAmC;AAAA,QACjD,eAAe,oBAAC,KAAA,EAAE,WAAU,sDAAsD,UAAA,aAAY;AAAA,QAG9F,SAAS,oBAAC,OAAA,EAAI,WAAU,uDAAuD,UAAA,MAAA,CAAM;AAAA,MAAA,GACxF;AAAA,MACC,WAAW,oBAAC,OAAA,EAAI,WAAU,2DAA2D,UAAA,QAAA,CAAQ;AAAA,IAAA,EAAA,CAChG;AAAA,EAAA,GACF;AAEJ;"}
@@ -0,0 +1,277 @@
1
+ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
2
+ import { useState, useEffect } from "react";
3
+ import { useLocation, NavLink } from "react-router-dom";
4
+ import { SunIcon, MoonIcon, MonitorIcon, CloseIcon, ChevronLeftIcon, PanelOpenIcon, ChevronDownIcon, GlobeIcon, LogOutIcon, ExternalLinkIcon } from "../../../foundations/icons/icons.js";
5
+ const SIDENAV_LABELS = {
6
+ closeMenu: "Fechar menu",
7
+ expandMenu: "Expandir menu",
8
+ collapseMenu: "Recolher menu",
9
+ openGroup: "Abrir",
10
+ closeGroup: "Fechar",
11
+ theme: "Tema",
12
+ light: "Claro",
13
+ dark: "Escuro",
14
+ system: "Sistema",
15
+ language: "Idioma",
16
+ logout: "Terminar sessão",
17
+ deployedAt: "Último deploy"
18
+ };
19
+ const DRAWER_MAX = 1024;
20
+ const isDrawer = () => typeof window !== "undefined" && window.innerWidth < DRAWER_MAX;
21
+ function formatDeploy(value) {
22
+ const d = value instanceof Date ? value : new Date(value);
23
+ if (Number.isNaN(d.getTime())) return "";
24
+ const two = (n) => String(n).padStart(2, "0");
25
+ return `${two(d.getDate())}/${two(d.getMonth() + 1)}/${d.getFullYear()}, ${two(d.getHours())}:${two(d.getMinutes())}`;
26
+ }
27
+ const BADGE_TONES = {
28
+ attention: "bg-amber-400 text-proef-navy",
29
+ alert: "bg-orange-500 text-white"
30
+ };
31
+ function SideNav({
32
+ menu,
33
+ groupLabels = {},
34
+ appTitle,
35
+ avatar,
36
+ onLogout,
37
+ languages,
38
+ currentLanguage,
39
+ onLanguageChange,
40
+ theme,
41
+ onThemeChange,
42
+ deployedAt,
43
+ footer,
44
+ labels: labelsProp,
45
+ open,
46
+ onOpenChange
47
+ }) {
48
+ const labels = { ...SIDENAV_LABELS, ...labelsProp };
49
+ const location = useLocation();
50
+ const [hovered, setHovered] = useState(false);
51
+ const [collapsed, setCollapsed] = useState({});
52
+ const expanded = open || hovered;
53
+ useEffect(() => {
54
+ const reset = () => setHovered(false);
55
+ window.addEventListener("blur", reset);
56
+ document.addEventListener("visibilitychange", reset);
57
+ return () => {
58
+ window.removeEventListener("blur", reset);
59
+ document.removeEventListener("visibilitychange", reset);
60
+ };
61
+ }, []);
62
+ const ungrouped = menu.filter((item) => !item.group);
63
+ const groups = Array.from(new Set(menu.map((item) => item.group).filter(Boolean)));
64
+ const activeGroup = menu.find((item) => item.group && !item.external && location.pathname.startsWith(item.to))?.group;
65
+ const closeIfDrawer = () => {
66
+ if (isDrawer()) onOpenChange(false);
67
+ };
68
+ const themeOptions = [
69
+ { value: "light", Icon: SunIcon, label: labels.light },
70
+ { value: "dark", Icon: MoonIcon, label: labels.dark },
71
+ { value: "system", Icon: MonitorIcon, label: labels.system }
72
+ ];
73
+ const currentTheme = themeOptions.find((o) => o.value === (theme ?? "system")) ?? themeOptions[2];
74
+ const cycle = (list, current) => list[(list.indexOf(current) + 1) % list.length];
75
+ const footerIconButton = "flex cursor-pointer items-center justify-center rounded-lg border-0 bg-transparent p-2 text-white/30 transition-colors hover:bg-white/5 hover:text-proef-turquoise focus-visible:ring-2 focus-visible:ring-proef-turquoise/50 focus-visible:outline-none";
76
+ const renderItem = (item) => {
77
+ const badges = (item.badges ?? []).filter((b) => b.count > 0);
78
+ const rowClass = (active) => `mx-2 flex items-center gap-4 rounded-lg no-underline transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-proef-turquoise/50 focus-visible:outline-none ${expanded ? "px-6 py-3 lg:py-2.5" : "justify-center px-0 py-3 lg:py-2.5"} ${active ? "bg-proef-turquoise/15 font-semibold text-proef-turquoise" : "text-white/50 hover:bg-white/5 hover:text-white"}`;
79
+ const content = (active) => /* @__PURE__ */ jsxs(Fragment, { children: [
80
+ /* @__PURE__ */ jsxs("span", { className: "relative flex-none", children: [
81
+ /* @__PURE__ */ jsx(item.icon, { size: 20, className: active ? "drop-shadow-[0_0_6px_rgba(0,230,200,0.6)]" : "" }),
82
+ badges.length > 0 && !expanded && /* @__PURE__ */ jsx("span", { className: "absolute -top-1 -right-1 h-2 w-2 rounded-full bg-amber-400" })
83
+ ] }),
84
+ expanded && /* @__PURE__ */ jsxs("span", { className: "flex min-w-0 flex-1 items-center gap-1.5", children: [
85
+ /* @__PURE__ */ jsx("span", { className: "truncate text-sm font-semibold", children: item.label }),
86
+ item.external && /* @__PURE__ */ jsx(ExternalLinkIcon, { size: 12, className: "ml-auto flex-none text-white/30" }),
87
+ badges.map((b, i) => /* @__PURE__ */ jsx(
88
+ "span",
89
+ {
90
+ "data-tip": b.label,
91
+ "aria-label": b.label ? `${b.label}: ${b.count}` : void 0,
92
+ className: `${i === 0 ? "ml-auto" : ""} min-w-[18px] flex-none rounded-full px-1.5 py-0.5 text-center text-[10px] leading-none font-black tabular-nums ${BADGE_TONES[b.tone ?? "attention"]}`,
93
+ children: b.count
94
+ },
95
+ i
96
+ ))
97
+ ] })
98
+ ] });
99
+ const tip = expanded ? void 0 : item.label;
100
+ if (item.external) {
101
+ return /* @__PURE__ */ jsx(
102
+ "a",
103
+ {
104
+ href: item.to,
105
+ target: "_blank",
106
+ rel: "noreferrer",
107
+ "data-tip": tip,
108
+ "aria-label": expanded ? void 0 : item.label,
109
+ onClick: closeIfDrawer,
110
+ className: rowClass(false),
111
+ children: content(false)
112
+ },
113
+ item.to
114
+ );
115
+ }
116
+ return /* @__PURE__ */ jsx(
117
+ NavLink,
118
+ {
119
+ to: item.to,
120
+ end: item.end,
121
+ "data-tip": tip,
122
+ "aria-label": expanded ? void 0 : item.label,
123
+ onClick: closeIfDrawer,
124
+ className: ({ isActive }) => rowClass(isActive),
125
+ children: ({ isActive }) => content(isActive)
126
+ },
127
+ item.to
128
+ );
129
+ };
130
+ return /* @__PURE__ */ jsxs(
131
+ "aside",
132
+ {
133
+ onPointerEnter: (e) => {
134
+ if (e.pointerType === "mouse" && !open) setHovered(true);
135
+ },
136
+ onPointerLeave: () => setHovered(false),
137
+ style: { top: "var(--app-shell-top, 0px)", height: "calc(100dvh - var(--app-shell-top, 0px))" },
138
+ className: `fixed left-0 z-40 flex w-64 flex-col overflow-hidden border-r border-white/10 bg-proef-navy text-white transition-all duration-300 ${open ? "translate-x-0" : "-translate-x-full"} lg:translate-x-0 ${expanded ? "lg:w-64" : "lg:w-20"}`,
139
+ children: [
140
+ /* @__PURE__ */ jsxs("div", { className: `flex h-16 flex-none items-center px-4 ${expanded ? "justify-between" : "justify-center"}`, children: [
141
+ expanded && /* @__PURE__ */ jsxs("div", { className: "flex flex-col pl-2", children: [
142
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
143
+ /* @__PURE__ */ jsx("span", { className: "text-3xl font-black tracking-tighter text-white", children: "proef" }),
144
+ /* @__PURE__ */ jsx("span", { className: "mt-3 h-2 w-2 rounded-full bg-proef-turquoise motion-safe:animate-pulse" })
145
+ ] }),
146
+ appTitle && /* @__PURE__ */ jsx("span", { className: "mt-1 pl-1 text-[10px] font-black tracking-[0.3em] text-white/50 uppercase", children: appTitle })
147
+ ] }),
148
+ /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2", children: [
149
+ avatar && /* @__PURE__ */ jsx("div", { className: "flex-none", children: avatar }),
150
+ open && expanded && /* @__PURE__ */ jsxs(
151
+ "button",
152
+ {
153
+ type: "button",
154
+ onClick: () => onOpenChange(false),
155
+ "aria-label": isDrawer() ? labels.closeMenu : labels.collapseMenu,
156
+ className: "flex cursor-pointer items-center justify-center rounded-lg border-0 bg-white/5 p-2.5 text-white/40 transition-colors hover:bg-white/10 hover:text-white focus-visible:ring-2 focus-visible:ring-proef-turquoise/50 focus-visible:outline-none lg:p-1.5",
157
+ children: [
158
+ /* @__PURE__ */ jsx(CloseIcon, { size: 18, className: "lg:hidden" }),
159
+ /* @__PURE__ */ jsx(ChevronLeftIcon, { size: 16, className: "hidden lg:block" })
160
+ ]
161
+ }
162
+ )
163
+ ] })
164
+ ] }),
165
+ /* @__PURE__ */ jsxs("nav", { className: "flex-1 overflow-x-hidden overflow-y-auto pt-2", children: [
166
+ !expanded && /* @__PURE__ */ jsx(
167
+ "button",
168
+ {
169
+ type: "button",
170
+ onClick: () => onOpenChange(true),
171
+ "data-tip": labels.expandMenu,
172
+ "aria-label": labels.expandMenu,
173
+ className: "mb-1 flex w-full cursor-pointer items-center justify-center border-0 bg-transparent py-3 text-white/40 transition-colors hover:bg-white/5 hover:text-white focus-visible:ring-2 focus-visible:ring-proef-turquoise/50 focus-visible:outline-none",
174
+ children: /* @__PURE__ */ jsx(PanelOpenIcon, { size: 20 })
175
+ }
176
+ ),
177
+ ungrouped.map(renderItem),
178
+ groups.map((g) => {
179
+ const isOpen = expanded ? !collapsed[g] : g === activeGroup;
180
+ const name = groupLabels[g] ?? g;
181
+ return /* @__PURE__ */ jsxs("div", { children: [
182
+ expanded ? /* @__PURE__ */ jsxs(
183
+ "button",
184
+ {
185
+ type: "button",
186
+ onClick: () => setCollapsed((prev) => ({ ...prev, [g]: !prev[g] })),
187
+ "aria-expanded": isOpen,
188
+ "aria-label": `${isOpen ? labels.closeGroup : labels.openGroup} ${name}`,
189
+ className: "group/grp w-full cursor-pointer border-0 bg-transparent px-6 pt-5 pb-3 text-left focus-visible:outline-none lg:pb-1.5",
190
+ children: [
191
+ /* @__PURE__ */ jsxs("span", { className: "flex items-center justify-between", children: [
192
+ /* @__PURE__ */ jsx("span", { className: "text-[9px] font-black tracking-[0.25em] text-white/30 uppercase transition-colors group-hover/grp:text-white/50 group-focus-visible/grp:text-white", children: name }),
193
+ /* @__PURE__ */ jsx(ChevronDownIcon, { size: 10, className: `text-white/30 transition-transform group-hover/grp:text-white/50 ${isOpen ? "" : "-rotate-90"}` })
194
+ ] }),
195
+ /* @__PURE__ */ jsx("span", { className: "mt-1 block h-px bg-white/10" })
196
+ ]
197
+ }
198
+ ) : isOpen && /* @__PURE__ */ jsx("div", { className: "mx-auto my-3 h-px w-8 bg-white/15" }),
199
+ isOpen && menu.filter((item) => item.group === g).map(renderItem)
200
+ ] }, g);
201
+ })
202
+ ] }),
203
+ /* @__PURE__ */ jsxs("div", { className: `mt-auto flex flex-none flex-col border-t border-white/10 px-2 py-3 ${expanded ? "gap-2" : "items-center gap-3"}`, children: [
204
+ footer && /* @__PURE__ */ jsx("div", { className: expanded ? "" : "flex w-full justify-center", children: footer(expanded) }),
205
+ onThemeChange && (expanded ? /* @__PURE__ */ jsxs("div", { children: [
206
+ /* @__PURE__ */ jsx("p", { className: "m-0 px-1 pb-1 text-[8px] font-black tracking-[0.25em] text-white/30 uppercase", children: labels.theme }),
207
+ /* @__PURE__ */ jsx("div", { role: "group", "aria-label": labels.theme, className: "grid grid-cols-3 gap-1 rounded-lg border border-white/5 bg-white/5 p-1", children: themeOptions.map(({ value, Icon, label }) => /* @__PURE__ */ jsxs(
208
+ "button",
209
+ {
210
+ type: "button",
211
+ onClick: () => onThemeChange(value),
212
+ "aria-pressed": currentTheme.value === value,
213
+ className: `flex cursor-pointer flex-col items-center gap-1 rounded-md border-0 py-1.5 text-[8px] font-black tracking-wider uppercase transition-colors focus-visible:ring-2 focus-visible:ring-proef-turquoise/50 focus-visible:outline-none ${currentTheme.value === value ? "bg-proef-turquoise/15 text-proef-turquoise" : "bg-transparent text-white/40 hover:bg-white/5 hover:text-white/70"}`,
214
+ children: [
215
+ /* @__PURE__ */ jsx(Icon, { size: 14 }),
216
+ label
217
+ ]
218
+ },
219
+ value
220
+ )) })
221
+ ] }) : /* @__PURE__ */ jsx(
222
+ "button",
223
+ {
224
+ type: "button",
225
+ onClick: () => onThemeChange(cycle(themeOptions, currentTheme).value),
226
+ "aria-label": `${labels.theme}: ${currentTheme.label}`,
227
+ "data-tip": `${labels.theme}: ${currentTheme.label}`,
228
+ className: footerIconButton,
229
+ children: /* @__PURE__ */ jsx(currentTheme.Icon, { size: 18 })
230
+ }
231
+ )),
232
+ languages && languages.length > 0 && onLanguageChange && (expanded ? /* @__PURE__ */ jsx("div", { role: "group", "aria-label": labels.language, className: "flex gap-1", children: languages.map((l) => /* @__PURE__ */ jsx(
233
+ "button",
234
+ {
235
+ type: "button",
236
+ onClick: () => onLanguageChange(l.code),
237
+ "aria-pressed": currentLanguage === l.code,
238
+ className: `flex-1 cursor-pointer rounded-md border bg-transparent py-1.5 text-[10px] font-black tracking-widest uppercase transition-colors focus-visible:ring-2 focus-visible:ring-proef-turquoise/50 focus-visible:outline-none ${currentLanguage === l.code ? "border-proef-turquoise/40 text-proef-turquoise" : "border-white/5 text-white/40 hover:border-white/20 hover:text-white"}`,
239
+ children: l.label
240
+ },
241
+ l.code
242
+ )) }) : /* @__PURE__ */ jsx(
243
+ "button",
244
+ {
245
+ type: "button",
246
+ onClick: () => {
247
+ const codes = languages.map((l) => l.code);
248
+ onLanguageChange(cycle(codes, currentLanguage ?? codes[0]));
249
+ },
250
+ "aria-label": `${labels.language}: ${currentLanguage?.toUpperCase() ?? ""}`,
251
+ "data-tip": `${labels.language}: ${currentLanguage?.toUpperCase() ?? ""}`,
252
+ className: footerIconButton,
253
+ children: /* @__PURE__ */ jsx(GlobeIcon, { size: 18 })
254
+ }
255
+ )),
256
+ onLogout && (expanded ? /* @__PURE__ */ jsx(
257
+ "button",
258
+ {
259
+ type: "button",
260
+ onClick: onLogout,
261
+ className: "w-full cursor-pointer rounded-lg border border-white/5 bg-transparent py-2.5 text-center text-[10px] font-black tracking-widest text-white/40 uppercase transition-colors hover:border-proef-turquoise/30 hover:text-proef-turquoise focus-visible:ring-2 focus-visible:ring-proef-turquoise/50 focus-visible:outline-none",
262
+ children: labels.logout
263
+ }
264
+ ) : /* @__PURE__ */ jsx("button", { type: "button", onClick: onLogout, "aria-label": labels.logout, "data-tip": labels.logout, className: footerIconButton, children: /* @__PURE__ */ jsx(LogOutIcon, { size: 18 }) })),
265
+ deployedAt != null && expanded && /* @__PURE__ */ jsx("p", { "data-tip": labels.deployedAt, className: "m-0 text-center text-[9px] tracking-wider text-white/25 tabular-nums select-text", children: formatDeploy(deployedAt) })
266
+ ] })
267
+ ]
268
+ }
269
+ );
270
+ }
271
+ export {
272
+ DRAWER_MAX,
273
+ SIDENAV_LABELS,
274
+ SideNav as default,
275
+ isDrawer
276
+ };
277
+ //# sourceMappingURL=SideNav.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"SideNav.js","sources":["../../../../../../src/v1/components/layout/SideNav/SideNav.tsx"],"sourcesContent":["import React, { useEffect, useState } from 'react'\r\nimport { NavLink, useLocation } from 'react-router-dom'\r\nimport {\r\n ChevronDownIcon,\r\n ChevronLeftIcon,\r\n CloseIcon,\r\n ExternalLinkIcon,\r\n GlobeIcon,\r\n LogOutIcon,\r\n MonitorIcon,\r\n MoonIcon,\r\n PanelOpenIcon,\r\n SunIcon,\r\n type IconProps,\r\n} from '../../../foundations/icons/icons'\r\n\r\nexport type SideNavTheme = 'light' | 'dark' | 'system'\r\n\r\nexport interface SideNavBadge {\r\n count: number\r\n /** `attention` (amarelo, por omissão): coisas à espera da pessoa. `alert` (laranja): à espera de outro circuito (ex: CAB). */\r\n tone?: 'attention' | 'alert'\r\n /** O que o número conta, para o tooltip e leitores de ecrã (ex: \"Aprovações pendentes\"). */\r\n label?: string\r\n}\r\n\r\nexport interface SideNavItem {\r\n /** Rota da app, ou URL completo com `external`. */\r\n to: string\r\n label: string\r\n /** Componente de ícone que aceita `size` e `className` (ex: um ícone do lucide-react). */\r\n icon: React.ComponentType<{ size?: number; className?: string }>\r\n /** Chave do grupo. Os itens do mesmo grupo aparecem juntos, por baixo do título do grupo. */\r\n group?: string\r\n /** Só fica ativo na rota exata (como o `end` do NavLink). */\r\n end?: boolean\r\n /** Abre num separador novo, fora da app. */\r\n external?: boolean\r\n /** Contadores ao lado do texto. Os que têm `count` 0 não aparecem. */\r\n badges?: SideNavBadge[]\r\n}\r\n\r\nexport interface SideNavLabels {\r\n closeMenu: string\r\n expandMenu: string\r\n collapseMenu: string\r\n /** Antes do nome do grupo, no botão que o abre/fecha (ex: \"Abrir Recursos\"). */\r\n openGroup: string\r\n closeGroup: string\r\n theme: string\r\n light: string\r\n dark: string\r\n system: string\r\n language: string\r\n logout: string\r\n deployedAt: string\r\n}\r\n\r\nexport const SIDENAV_LABELS: SideNavLabels = {\r\n closeMenu: 'Fechar menu',\r\n expandMenu: 'Expandir menu',\r\n collapseMenu: 'Recolher menu',\r\n openGroup: 'Abrir',\r\n closeGroup: 'Fechar',\r\n theme: 'Tema',\r\n light: 'Claro',\r\n dark: 'Escuro',\r\n system: 'Sistema',\r\n language: 'Idioma',\r\n logout: 'Terminar sessão',\r\n deployedAt: 'Último deploy',\r\n}\r\n\r\nexport interface SideNavProps {\r\n menu: SideNavItem[]\r\n /** Título de cada grupo do menu, pela chave (`group`). Sem título, aparece a chave. */\r\n groupLabels?: Record<string, string>\r\n /** Por baixo do logótipo (ex: \"IT Portal\"). */\r\n appTitle?: string\r\n /** Foto ou iniciais da pessoa, no topo do menu. */\r\n avatar?: React.ReactNode\r\n /** Sem isto, não há botão de terminar sessão. */\r\n onLogout?: () => void\r\n /** Escolha de idioma no rodapé. Só aparece com `languages` e `onLanguageChange`. */\r\n languages?: { code: string; label: string }[]\r\n currentLanguage?: string\r\n onLanguageChange?: (code: string) => void\r\n /**\r\n * Escolha de tema no rodapé. Só aparece com `onThemeChange`. O menu só\r\n * mostra a escolha: pôr a classe `.dark` no `<html>` é da app.\r\n */\r\n theme?: SideNavTheme\r\n onThemeChange?: (theme: SideNavTheme) => void\r\n /** Quando foi feito o build, no fundo do menu. A app decide a quem o mostra (ex: só administradores). */\r\n deployedAt?: string | number | Date\r\n /** Conteúdo extra no rodapé, por cima do tema (ex: um filtro global). Recebe se o menu está expandido. */\r\n footer?: (expanded: boolean) => React.ReactNode\r\n labels?: Partial<SideNavLabels>\r\n /** Aberto (256px) ou fechado (só ícones; em ecrãs pequenos, escondido). A app guarda este estado. */\r\n open: boolean\r\n onOpenChange: (open: boolean) => void\r\n}\r\n\r\n/** Abaixo desta largura o menu é uma gaveta por cima da página; acima ocupa espaço (aberto ou só ícones). */\r\nexport const DRAWER_MAX = 1024\r\n\r\nexport const isDrawer = () => typeof window !== 'undefined' && window.innerWidth < DRAWER_MAX\r\n\r\n/** `DD/MM/AAAA, HH:MM` na hora local: um carimbo que não muda com o idioma escolhido. */\r\nfunction formatDeploy(value: string | number | Date): string {\r\n const d = value instanceof Date ? value : new Date(value)\r\n if (Number.isNaN(d.getTime())) return ''\r\n const two = (n: number) => String(n).padStart(2, '0')\r\n return `${two(d.getDate())}/${two(d.getMonth() + 1)}/${d.getFullYear()}, ${two(d.getHours())}:${two(d.getMinutes())}`\r\n}\r\n\r\nconst BADGE_TONES = {\r\n attention: 'bg-amber-400 text-proef-navy',\r\n alert: 'bg-orange-500 text-white',\r\n}\r\n\r\n/**\r\n * Menu lateral navy da app, fixo à esquerda. Normalmente usa-se através do\r\n * `AppShell`, que também trata da página ao lado.\r\n *\r\n * - **Ecrãs grandes** (≥ 1024px): aberto ocupa 256px; fechado fica só com os\r\n * ícones (80px) e abre por cima da página ao passar o rato.\r\n * - **Ecrãs pequenos**: escondido quando fechado; aberto fica por cima da\r\n * página e fecha ao escolher uma página.\r\n * - **Grupos**: abertos por omissão, fecham com um clique no título. Só com os\r\n * ícones, fica aberto apenas o grupo da página atual.\r\n *\r\n * Precisa de estar dentro de um Router do react-router.\r\n */\r\nexport default function SideNav({\r\n menu,\r\n groupLabels = {},\r\n appTitle,\r\n avatar,\r\n onLogout,\r\n languages,\r\n currentLanguage,\r\n onLanguageChange,\r\n theme,\r\n onThemeChange,\r\n deployedAt,\r\n footer,\r\n labels: labelsProp,\r\n open,\r\n onOpenChange,\r\n}: SideNavProps) {\r\n const labels = { ...SIDENAV_LABELS, ...labelsProp }\r\n const location = useLocation()\r\n const [hovered, setHovered] = useState(false)\r\n const [collapsed, setCollapsed] = useState<Record<string, boolean>>({})\r\n const expanded = open || hovered\r\n\r\n // O hover fica \"colado\" quando a janela perde o foco.\r\n useEffect(() => {\r\n const reset = () => setHovered(false)\r\n window.addEventListener('blur', reset)\r\n document.addEventListener('visibilitychange', reset)\r\n return () => {\r\n window.removeEventListener('blur', reset)\r\n document.removeEventListener('visibilitychange', reset)\r\n }\r\n }, [])\r\n\r\n const ungrouped = menu.filter((item) => !item.group)\r\n const groups = Array.from(new Set(menu.map((item) => item.group).filter(Boolean))) as string[]\r\n // Só com os ícones não há títulos para clicar: fica aberto só o grupo da página atual.\r\n const activeGroup = menu.find((item) => item.group && !item.external && location.pathname.startsWith(item.to))?.group\r\n\r\n const closeIfDrawer = () => {\r\n if (isDrawer()) onOpenChange(false)\r\n }\r\n\r\n const themeOptions: { value: SideNavTheme; Icon: React.ComponentType<IconProps>; label: string }[] = [\r\n { value: 'light', Icon: SunIcon, label: labels.light },\r\n { value: 'dark', Icon: MoonIcon, label: labels.dark },\r\n { value: 'system', Icon: MonitorIcon, label: labels.system },\r\n ]\r\n const currentTheme = themeOptions.find((o) => o.value === (theme ?? 'system')) ?? themeOptions[2]\r\n const cycle = <T,>(list: T[], current: T) => list[(list.indexOf(current) + 1) % list.length]\r\n\r\n const footerIconButton =\r\n 'flex cursor-pointer items-center justify-center rounded-lg border-0 bg-transparent p-2 text-white/30 transition-colors hover:bg-white/5 hover:text-proef-turquoise focus-visible:ring-2 focus-visible:ring-proef-turquoise/50 focus-visible:outline-none'\r\n\r\n const renderItem = (item: SideNavItem) => {\r\n const badges = (item.badges ?? []).filter((b) => b.count > 0)\r\n const rowClass = (active: boolean) =>\r\n `mx-2 flex items-center gap-4 rounded-lg no-underline transition-colors duration-200 focus-visible:ring-2 focus-visible:ring-proef-turquoise/50 focus-visible:outline-none ${\r\n expanded ? 'px-6 py-3 lg:py-2.5' : 'justify-center px-0 py-3 lg:py-2.5'\r\n } ${active ? 'bg-proef-turquoise/15 font-semibold text-proef-turquoise' : 'text-white/50 hover:bg-white/5 hover:text-white'}`\r\n\r\n const content = (active: boolean) => (\r\n <>\r\n <span className=\"relative flex-none\">\r\n <item.icon size={20} className={active ? 'drop-shadow-[0_0_6px_rgba(0,230,200,0.6)]' : ''} />\r\n {badges.length > 0 && !expanded && <span className=\"absolute -top-1 -right-1 h-2 w-2 rounded-full bg-amber-400\" />}\r\n </span>\r\n {expanded && (\r\n <span className=\"flex min-w-0 flex-1 items-center gap-1.5\">\r\n <span className=\"truncate text-sm font-semibold\">{item.label}</span>\r\n {item.external && <ExternalLinkIcon size={12} className=\"ml-auto flex-none text-white/30\" />}\r\n {badges.map((b, i) => (\r\n <span\r\n key={i}\r\n data-tip={b.label}\r\n aria-label={b.label ? `${b.label}: ${b.count}` : undefined}\r\n className={`${i === 0 ? 'ml-auto' : ''} min-w-[18px] flex-none rounded-full px-1.5 py-0.5 text-center text-[10px] leading-none font-black tabular-nums ${BADGE_TONES[b.tone ?? 'attention']}`}\r\n >\r\n {b.count}\r\n </span>\r\n ))}\r\n </span>\r\n )}\r\n </>\r\n )\r\n\r\n const tip = expanded ? undefined : item.label\r\n if (item.external) {\r\n return (\r\n <a\r\n key={item.to}\r\n href={item.to}\r\n target=\"_blank\"\r\n rel=\"noreferrer\"\r\n data-tip={tip}\r\n aria-label={expanded ? undefined : item.label}\r\n onClick={closeIfDrawer}\r\n className={rowClass(false)}\r\n >\r\n {content(false)}\r\n </a>\r\n )\r\n }\r\n return (\r\n <NavLink\r\n key={item.to}\r\n to={item.to}\r\n end={item.end}\r\n data-tip={tip}\r\n aria-label={expanded ? undefined : item.label}\r\n onClick={closeIfDrawer}\r\n className={({ isActive }) => rowClass(isActive)}\r\n >\r\n {({ isActive }) => content(isActive)}\r\n </NavLink>\r\n )\r\n }\r\n\r\n return (\r\n <aside\r\n // Abrir por hover só com rato: em ecrãs táteis o hover fica \"colado\".\r\n onPointerEnter={(e) => {\r\n if (e.pointerType === 'mouse' && !open) setHovered(true)\r\n }}\r\n onPointerLeave={() => setHovered(false)}\r\n // As barras do topo do AppShell (se houver) empurram o menu para baixo.\r\n style={{ top: 'var(--app-shell-top, 0px)', height: 'calc(100dvh - var(--app-shell-top, 0px))' }}\r\n className={\r\n 'fixed left-0 z-40 flex w-64 flex-col overflow-hidden border-r border-white/10 bg-proef-navy text-white transition-all duration-300 ' +\r\n `${open ? 'translate-x-0' : '-translate-x-full'} lg:translate-x-0 ${expanded ? 'lg:w-64' : 'lg:w-20'}`\r\n }\r\n >\r\n <div className={`flex h-16 flex-none items-center px-4 ${expanded ? 'justify-between' : 'justify-center'}`}>\r\n {expanded && (\r\n <div className=\"flex flex-col pl-2\">\r\n <div className=\"flex items-center gap-2\">\r\n <span className=\"text-3xl font-black tracking-tighter text-white\">proef</span>\r\n <span className=\"mt-3 h-2 w-2 rounded-full bg-proef-turquoise motion-safe:animate-pulse\" />\r\n </div>\r\n {appTitle && <span className=\"mt-1 pl-1 text-[10px] font-black tracking-[0.3em] text-white/50 uppercase\">{appTitle}</span>}\r\n </div>\r\n )}\r\n <div className=\"flex items-center gap-2\">\r\n {avatar && <div className=\"flex-none\">{avatar}</div>}\r\n {open && expanded && (\r\n <button\r\n type=\"button\"\r\n onClick={() => onOpenChange(false)}\r\n aria-label={isDrawer() ? labels.closeMenu : labels.collapseMenu}\r\n className=\"flex cursor-pointer items-center justify-center rounded-lg border-0 bg-white/5 p-2.5 text-white/40 transition-colors hover:bg-white/10 hover:text-white focus-visible:ring-2 focus-visible:ring-proef-turquoise/50 focus-visible:outline-none lg:p-1.5\"\r\n >\r\n <CloseIcon size={18} className=\"lg:hidden\" />\r\n <ChevronLeftIcon size={16} className=\"hidden lg:block\" />\r\n </button>\r\n )}\r\n </div>\r\n </div>\r\n\r\n <nav className=\"flex-1 overflow-x-hidden overflow-y-auto pt-2\">\r\n {/* Só ícones: botão para abrir (em ecrãs táteis não há hover). */}\r\n {!expanded && (\r\n <button\r\n type=\"button\"\r\n onClick={() => onOpenChange(true)}\r\n data-tip={labels.expandMenu}\r\n aria-label={labels.expandMenu}\r\n className=\"mb-1 flex w-full cursor-pointer items-center justify-center border-0 bg-transparent py-3 text-white/40 transition-colors hover:bg-white/5 hover:text-white focus-visible:ring-2 focus-visible:ring-proef-turquoise/50 focus-visible:outline-none\"\r\n >\r\n <PanelOpenIcon size={20} />\r\n </button>\r\n )}\r\n {ungrouped.map(renderItem)}\r\n {groups.map((g) => {\r\n const isOpen = expanded ? !collapsed[g] : g === activeGroup\r\n const name = groupLabels[g] ?? g\r\n return (\r\n <div key={g}>\r\n {expanded ? (\r\n <button\r\n type=\"button\"\r\n onClick={() => setCollapsed((prev) => ({ ...prev, [g]: !prev[g] }))}\r\n aria-expanded={isOpen}\r\n aria-label={`${isOpen ? labels.closeGroup : labels.openGroup} ${name}`}\r\n className=\"group/grp w-full cursor-pointer border-0 bg-transparent px-6 pt-5 pb-3 text-left focus-visible:outline-none lg:pb-1.5\"\r\n >\r\n <span className=\"flex items-center justify-between\">\r\n <span className=\"text-[9px] font-black tracking-[0.25em] text-white/30 uppercase transition-colors group-hover/grp:text-white/50 group-focus-visible/grp:text-white\">\r\n {name}\r\n </span>\r\n <ChevronDownIcon size={10} className={`text-white/30 transition-transform group-hover/grp:text-white/50 ${isOpen ? '' : '-rotate-90'}`} />\r\n </span>\r\n <span className=\"mt-1 block h-px bg-white/10\" />\r\n </button>\r\n ) : (\r\n isOpen && <div className=\"mx-auto my-3 h-px w-8 bg-white/15\" />\r\n )}\r\n {isOpen && menu.filter((item) => item.group === g).map(renderItem)}\r\n </div>\r\n )\r\n })}\r\n </nav>\r\n\r\n <div className={`mt-auto flex flex-none flex-col border-t border-white/10 px-2 py-3 ${expanded ? 'gap-2' : 'items-center gap-3'}`}>\r\n {footer && <div className={expanded ? '' : 'flex w-full justify-center'}>{footer(expanded)}</div>}\r\n\r\n {onThemeChange &&\r\n (expanded ? (\r\n <div>\r\n <p className=\"m-0 px-1 pb-1 text-[8px] font-black tracking-[0.25em] text-white/30 uppercase\">{labels.theme}</p>\r\n <div role=\"group\" aria-label={labels.theme} className=\"grid grid-cols-3 gap-1 rounded-lg border border-white/5 bg-white/5 p-1\">\r\n {themeOptions.map(({ value, Icon, label }) => (\r\n <button\r\n key={value}\r\n type=\"button\"\r\n onClick={() => onThemeChange(value)}\r\n aria-pressed={currentTheme.value === value}\r\n className={`flex cursor-pointer flex-col items-center gap-1 rounded-md border-0 py-1.5 text-[8px] font-black tracking-wider uppercase transition-colors focus-visible:ring-2 focus-visible:ring-proef-turquoise/50 focus-visible:outline-none ${\r\n currentTheme.value === value ? 'bg-proef-turquoise/15 text-proef-turquoise' : 'bg-transparent text-white/40 hover:bg-white/5 hover:text-white/70'\r\n }`}\r\n >\r\n <Icon size={14} />\r\n {label}\r\n </button>\r\n ))}\r\n </div>\r\n </div>\r\n ) : (\r\n <button\r\n type=\"button\"\r\n onClick={() => onThemeChange(cycle(themeOptions, currentTheme).value)}\r\n aria-label={`${labels.theme}: ${currentTheme.label}`}\r\n data-tip={`${labels.theme}: ${currentTheme.label}`}\r\n className={footerIconButton}\r\n >\r\n <currentTheme.Icon size={18} />\r\n </button>\r\n ))}\r\n\r\n {languages &&\r\n languages.length > 0 &&\r\n onLanguageChange &&\r\n (expanded ? (\r\n <div role=\"group\" aria-label={labels.language} className=\"flex gap-1\">\r\n {languages.map((l) => (\r\n <button\r\n key={l.code}\r\n type=\"button\"\r\n onClick={() => onLanguageChange(l.code)}\r\n aria-pressed={currentLanguage === l.code}\r\n className={`flex-1 cursor-pointer rounded-md border bg-transparent py-1.5 text-[10px] font-black tracking-widest uppercase transition-colors focus-visible:ring-2 focus-visible:ring-proef-turquoise/50 focus-visible:outline-none ${\r\n currentLanguage === l.code ? 'border-proef-turquoise/40 text-proef-turquoise' : 'border-white/5 text-white/40 hover:border-white/20 hover:text-white'\r\n }`}\r\n >\r\n {l.label}\r\n </button>\r\n ))}\r\n </div>\r\n ) : (\r\n <button\r\n type=\"button\"\r\n onClick={() => {\r\n const codes = languages.map((l) => l.code)\r\n onLanguageChange(cycle(codes, currentLanguage ?? codes[0]))\r\n }}\r\n aria-label={`${labels.language}: ${currentLanguage?.toUpperCase() ?? ''}`}\r\n data-tip={`${labels.language}: ${currentLanguage?.toUpperCase() ?? ''}`}\r\n className={footerIconButton}\r\n >\r\n <GlobeIcon size={18} />\r\n </button>\r\n ))}\r\n\r\n {onLogout &&\r\n (expanded ? (\r\n <button\r\n type=\"button\"\r\n onClick={onLogout}\r\n className=\"w-full cursor-pointer rounded-lg border border-white/5 bg-transparent py-2.5 text-center text-[10px] font-black tracking-widest text-white/40 uppercase transition-colors hover:border-proef-turquoise/30 hover:text-proef-turquoise focus-visible:ring-2 focus-visible:ring-proef-turquoise/50 focus-visible:outline-none\"\r\n >\r\n {labels.logout}\r\n </button>\r\n ) : (\r\n <button type=\"button\" onClick={onLogout} aria-label={labels.logout} data-tip={labels.logout} className={footerIconButton}>\r\n <LogOutIcon size={18} />\r\n </button>\r\n ))}\r\n\r\n {/* Só com o menu aberto: só com ícones ficava ilegível, e é para consulta. */}\r\n {deployedAt != null && expanded && (\r\n <p data-tip={labels.deployedAt} className=\"m-0 text-center text-[9px] tracking-wider text-white/25 tabular-nums select-text\">\r\n {formatDeploy(deployedAt)}\r\n </p>\r\n )}\r\n </div>\r\n </aside>\r\n )\r\n}\r\n"],"names":[],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ import { createContext, useContext } from "react";
2
+ const NavTrailContext = createContext(null);
3
+ const useNavTrail = () => useContext(NavTrailContext);
4
+ function findNavTrail(menu, groupLabels, pathname) {
5
+ let best;
6
+ for (const item of menu) {
7
+ if (item.external) continue;
8
+ const match = item.end ? pathname === item.to : pathname === item.to || pathname.startsWith(item.to.endsWith("/") ? item.to : `${item.to}/`);
9
+ if (match && (!best || item.to.length > best.to.length)) best = item;
10
+ }
11
+ if (!best) return null;
12
+ return { group: best.group ? groupLabels[best.group] ?? best.group : void 0, page: best.label };
13
+ }
14
+ export {
15
+ NavTrailContext,
16
+ findNavTrail,
17
+ useNavTrail
18
+ };
19
+ //# sourceMappingURL=navTrail.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"navTrail.js","sources":["../../../../../src/v1/components/layout/navTrail.ts"],"sourcesContent":["import { createContext, useContext } from 'react'\r\nimport type { SideNavItem } from './SideNav/SideNav'\r\n\r\n/** Onde a página atual está no menu: o grupo (se tiver) e o nome da entrada. Uso interno. */\r\nexport interface NavTrail {\r\n group?: string\r\n page: string\r\n}\r\n\r\nexport const NavTrailContext = createContext<NavTrail | null>(null)\r\n\r\n/** O lugar da página atual no menu do `AppShell`; `null` fora de um `AppShell` ou fora do menu. */\r\nexport const useNavTrail = () => useContext(NavTrailContext)\r\n\r\n/**\r\n * A entrada do menu da rota atual: a de caminho mais comprido que é o início da\r\n * rota (ex: `/equipamentos/rececao` antes de `/equipamentos`). Ligações\r\n * externas não contam.\r\n */\r\nexport function findNavTrail(menu: SideNavItem[], groupLabels: Record<string, string>, pathname: string): NavTrail | null {\r\n let best: SideNavItem | undefined\r\n for (const item of menu) {\r\n if (item.external) continue\r\n const match = item.end ? pathname === item.to : pathname === item.to || pathname.startsWith(item.to.endsWith('/') ? item.to : `${item.to}/`)\r\n if (match && (!best || item.to.length > best.to.length)) best = item\r\n }\r\n if (!best) return null\r\n return { group: best.group ? (groupLabels[best.group] ?? best.group) : undefined, page: best.label }\r\n}\r\n"],"names":[],"mappings":";AASO,MAAM,kBAAkB,cAA+B,IAAI;AAG3D,MAAM,cAAc,MAAM,WAAW,eAAe;AAOpD,SAAS,aAAa,MAAqB,aAAqC,UAAmC;AACxH,MAAI;AACJ,aAAW,QAAQ,MAAM;AACvB,QAAI,KAAK,SAAU;AACnB,UAAM,QAAQ,KAAK,MAAM,aAAa,KAAK,KAAK,aAAa,KAAK,MAAM,SAAS,WAAW,KAAK,GAAG,SAAS,GAAG,IAAI,KAAK,KAAK,GAAG,KAAK,EAAE,GAAG;AAC3I,QAAI,UAAU,CAAC,QAAQ,KAAK,GAAG,SAAS,KAAK,GAAG,QAAS,QAAO;AAAA,EAClE;AACA,MAAI,CAAC,KAAM,QAAO;AAClB,SAAO,EAAE,OAAO,KAAK,QAAS,YAAY,KAAK,KAAK,KAAK,KAAK,QAAS,QAAW,MAAM,KAAK,MAAA;AAC/F;"}