@proef/common-ui 2.2.6 → 2.5.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 (51) hide show
  1. package/dist/esm/index.js +6 -0
  2. package/dist/esm/index.js.map +1 -1
  3. package/dist/esm/legacy/components/charts/Charts.js +52 -192
  4. package/dist/esm/legacy/components/charts/Charts.js.map +1 -1
  5. package/dist/esm/legacy/components/molecules/DataTable/DataTable.js +252 -115
  6. package/dist/esm/legacy/components/molecules/DataTable/DataTable.js.map +1 -1
  7. package/dist/esm/legacy/components/organisms/GanttChart/GanttChart.js +7 -4
  8. package/dist/esm/legacy/components/organisms/GanttChart/GanttChart.js.map +1 -1
  9. package/dist/esm/v1/capabilities/overlay/useFloatingPanel.js +5 -2
  10. package/dist/esm/v1/capabilities/overlay/useFloatingPanel.js.map +1 -1
  11. package/dist/esm/v1/components/data/Charts/Charts.js +301 -0
  12. package/dist/esm/v1/components/data/Charts/Charts.js.map +1 -0
  13. package/dist/esm/v1/components/data/Pagination/Pagination.js +30 -26
  14. package/dist/esm/v1/components/data/Pagination/Pagination.js.map +1 -1
  15. package/dist/esm/v1/components/forms/FormField/FormField.js +3 -2
  16. package/dist/esm/v1/components/forms/FormField/FormField.js.map +1 -1
  17. package/dist/esm/v1/components/layout/AppShell/AppShell.js +8 -3
  18. package/dist/esm/v1/components/layout/AppShell/AppShell.js.map +1 -1
  19. package/dist/esm/v1/components/layout/Page/Page.js +1 -1
  20. package/dist/esm/v1/components/layout/Page/Page.js.map +1 -1
  21. package/dist/esm/v1/components/layout/PageHeader/PageHeader.js +21 -18
  22. package/dist/esm/v1/components/layout/PageHeader/PageHeader.js.map +1 -1
  23. package/dist/esm/v1/components/lists/ListFinder/ListFinder.js +16 -16
  24. package/dist/esm/v1/components/lists/ListFinder/ListFinder.js.map +1 -1
  25. package/dist/esm/v1/components/overlays/Dialog/Dialog.js +5 -2
  26. package/dist/esm/v1/components/overlays/Dialog/Dialog.js.map +1 -1
  27. package/dist/esm/v1/components/tables/Table/ColumnFilter.js +3 -1
  28. package/dist/esm/v1/components/tables/Table/ColumnFilter.js.map +1 -1
  29. package/dist/esm/v1/primitives/buttons/SegmentedControl/SegmentedControl.js +3 -3
  30. package/dist/esm/v1/primitives/buttons/SegmentedControl/SegmentedControl.js.map +1 -1
  31. package/dist/esm/v1/primitives/display/Kpi/Kpi.js +6 -3
  32. package/dist/esm/v1/primitives/display/Kpi/Kpi.js.map +1 -1
  33. package/dist/esm/v1/primitives/forms/SelectInput/SelectInput.js +31 -9
  34. package/dist/esm/v1/primitives/forms/SelectInput/SelectInput.js.map +1 -1
  35. package/dist/esm/v1/primitives/lists/ListItem/ListItem.js +5 -5
  36. package/dist/esm/v1/primitives/lists/ListItem/ListItem.js.map +1 -1
  37. package/dist/esm/v1/primitives/navigation/Tabs/Tabs.js +97 -36
  38. package/dist/esm/v1/primitives/navigation/Tabs/Tabs.js.map +1 -1
  39. package/dist/style.components.css +1 -1
  40. package/dist/style.css +1 -1
  41. package/dist/style.layered.css +1 -1
  42. package/dist/theme.css +28 -0
  43. package/dist/types/legacy/components/charts/Charts.d.ts +14 -11
  44. package/dist/types/legacy/components/molecules/DataTable/DataTable.d.ts +12 -1
  45. package/dist/types/legacy/components/organisms/GanttChart/GanttChart.d.ts +15 -1
  46. package/dist/types/legacy/exports/organisms.d.ts +1 -1
  47. package/dist/types/v1/components/data/Charts/Charts.d.ts +103 -0
  48. package/dist/types/v1/index.d.ts +2 -0
  49. package/dist/types/v1/primitives/forms/SelectInput/SelectInput.d.ts +12 -0
  50. package/dist/types/v1/primitives/lists/ListItem/ListItem.d.ts +1 -1
  51. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"Pagination.js","sources":["../../../../../../src/v1/components/data/Pagination/Pagination.tsx"],"sourcesContent":["import React from 'react'\r\nimport Button from '../../../primitives/buttons/Button/Button'\r\nimport SelectInput from '../../../primitives/forms/SelectInput/SelectInput'\r\n\r\nexport interface PaginationLabels {\r\n /** \"{page}\" e \"{pages}\" são substituídos (ex: \"Página {page} de {pages}\"). */\r\n page: string\r\n /** \"{count}\" é substituído pelo total de registos (ex: \"{count} resultados\"). */\r\n total: string\r\n perPage: string\r\n prev: string\r\n next: string\r\n}\r\n\r\nexport const DEFAULT_PAGINATION_LABELS: PaginationLabels = {\r\n page: 'Page {page} of {pages}',\r\n total: '{count} results',\r\n perPage: 'Per page',\r\n prev: 'Previous',\r\n next: 'Next',\r\n}\r\n\r\nexport interface PaginationProps {\r\n /** Página atual, a contar de 1. */\r\n page: number\r\n /** Número de páginas. */\r\n pages: number\r\n onPageChange: (page: number) => void\r\n /** Total de registos (mostrado à esquerda). */\r\n total?: number\r\n /** Registos por página e as opções do seletor (sem opções, não há seletor). */\r\n pageSize?: number\r\n pageSizeOptions?: number[]\r\n onPageSizeChange?: (size: number) => void\r\n labels?: Partial<PaginationLabels>\r\n className?: string\r\n}\r\n\r\nfunction ChevronLeft() {\r\n return (\r\n <svg aria-hidden viewBox=\"0 0 24 24\" width={13} height={13} fill=\"none\" stroke=\"currentColor\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\">\r\n <path d=\"m15 18-6-6 6-6\" />\r\n </svg>\r\n )\r\n}\r\n\r\nfunction ChevronRight() {\r\n return (\r\n <svg aria-hidden viewBox=\"0 0 24 24\" width={12} height={12} fill=\"none\" stroke=\"currentColor\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\">\r\n <path d=\"m9 18 6-6-6-6\" />\r\n </svg>\r\n )\r\n}\r\n\r\n/**\r\n * Paginação por baixo de uma lista: \"Página 1 de 4\", o total, quantos por página e\r\n * Anterior/Seguinte. É a mesma que a `DataTable` mostra; usar esta quando a app pagina\r\n * no servidor (a tabela recebe só a página atual).\r\n */\r\nexport default function Pagination({\r\n page,\r\n pages,\r\n onPageChange,\r\n total,\r\n pageSize,\r\n pageSizeOptions,\r\n onPageSizeChange,\r\n labels,\r\n className = '',\r\n}: PaginationProps) {\r\n const L = { ...DEFAULT_PAGINATION_LABELS, ...labels }\r\n const totalPaginas = Math.max(1, pages)\r\n const opcoes = pageSizeOptions && pageSize != null ? [...new Set([...pageSizeOptions, pageSize])].sort((a, b) => a - b) : []\r\n return (\r\n <nav aria-label={L.page.replace('{page}', String(page)).replace('{pages}', String(totalPaginas))} className={`flex flex-wrap items-center justify-between gap-2 ${className}`}>\r\n <span className=\"text-[10px] font-black uppercase tracking-widest text-muted-foreground\">\r\n {total != null && <>{L.total.replace('{count}', total.toLocaleString())} · </>}\r\n {L.page.replace('{page}', String(page)).replace('{pages}', String(totalPaginas))}\r\n </span>\r\n <div className=\"flex flex-wrap items-center gap-1.5\">\r\n {opcoes.length > 0 && onPageSizeChange && (\r\n <label className=\"mr-2 flex items-center gap-1.5 text-[10px] font-black uppercase tracking-widest text-muted-foreground\">\r\n {L.perPage}\r\n <SelectInput\r\n aria-label={L.perPage}\r\n value={String(pageSize)}\r\n onChange={(e) => onPageSizeChange(Number(e.target.value))}\r\n className=\"w-20 normal-case tracking-normal font-normal\"\r\n >\r\n {opcoes.map((n) => (\r\n <option key={n} value={n}>\r\n {n}\r\n </option>\r\n ))}\r\n </SelectInput>\r\n </label>\r\n )}\r\n <Button variant=\"secondary\" size=\"sm\" icon={<ChevronLeft />} disabled={page <= 1} onClick={() => onPageChange(page - 1)}>\r\n {L.prev}\r\n </Button>\r\n <Button variant=\"secondary\" size=\"sm\" disabled={page >= totalPaginas} onClick={() => onPageChange(page + 1)}>\r\n {L.next}\r\n <ChevronRight />\r\n </Button>\r\n </div>\r\n </nav>\r\n )\r\n}\r\n\r\nexport { Pagination }\r\n"],"names":[],"mappings":";;;AAcO,MAAM,4BAA8C;AAAA,EACzD,MAAM;AAAA,EACN,OAAO;AAAA,EACP,SAAS;AAAA,EACT,MAAM;AAAA,EACN,MAAM;AACR;AAkBA,SAAS,cAAc;AACrB,SACE,oBAAC,OAAA,EAAI,eAAW,MAAC,SAAQ,aAAY,OAAO,IAAI,QAAQ,IAAI,MAAK,QAAO,QAAO,gBAAe,aAAa,GAAG,eAAc,SAAQ,gBAAe,SACjJ,UAAA,oBAAC,QAAA,EAAK,GAAE,iBAAA,CAAiB,EAAA,CAC3B;AAEJ;AAEA,SAAS,eAAe;AACtB,SACE,oBAAC,OAAA,EAAI,eAAW,MAAC,SAAQ,aAAY,OAAO,IAAI,QAAQ,IAAI,MAAK,QAAO,QAAO,gBAAe,aAAa,GAAG,eAAc,SAAQ,gBAAe,SACjJ,UAAA,oBAAC,QAAA,EAAK,GAAE,gBAAA,CAAgB,EAAA,CAC1B;AAEJ;AAOA,SAAwB,WAAW;AAAA,EACjC;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA,YAAY;AACd,GAAoB;AAClB,QAAM,IAAI,EAAE,GAAG,2BAA2B,GAAG,OAAA;AAC7C,QAAM,eAAe,KAAK,IAAI,GAAG,KAAK;AACtC,QAAM,SAAS,mBAAmB,YAAY,OAAO,CAAC,GAAG,oBAAI,IAAI,CAAC,GAAG,iBAAiB,QAAQ,CAAC,CAAC,EAAE,KAAK,CAAC,GAAG,MAAM,IAAI,CAAC,IAAI,CAAA;AAC1H,SACE,qBAAC,SAAI,cAAY,EAAE,KAAK,QAAQ,UAAU,OAAO,IAAI,CAAC,EAAE,QAAQ,WAAW,OAAO,YAAY,CAAC,GAAG,WAAW,qDAAqD,SAAS,IACzK,UAAA;AAAA,IAAA,qBAAC,QAAA,EAAK,WAAU,0EACb,UAAA;AAAA,MAAA,SAAS,QAAQ,qBAAA,UAAA,EAAG,UAAA;AAAA,QAAA,EAAE,MAAM,QAAQ,WAAW,MAAM,gBAAgB;AAAA,QAAE;AAAA,MAAA,GAAG;AAAA,MAC1E,EAAE,KAAK,QAAQ,UAAU,OAAO,IAAI,CAAC,EAAE,QAAQ,WAAW,OAAO,YAAY,CAAC;AAAA,IAAA,GACjF;AAAA,IACA,qBAAC,OAAA,EAAI,WAAU,uCACZ,UAAA;AAAA,MAAA,OAAO,SAAS,KAAK,oBACpB,qBAAC,SAAA,EAAM,WAAU,yGACd,UAAA;AAAA,QAAA,EAAE;AAAA,QACH;AAAA,UAAC;AAAA,UAAA;AAAA,YACC,cAAY,EAAE;AAAA,YACd,OAAO,OAAO,QAAQ;AAAA,YACtB,UAAU,CAAC,MAAM,iBAAiB,OAAO,EAAE,OAAO,KAAK,CAAC;AAAA,YACxD,WAAU;AAAA,YAET,UAAA,OAAO,IAAI,CAAC,MACX,oBAAC,YAAe,OAAO,GACpB,UAAA,EAAA,GADU,CAEb,CACD;AAAA,UAAA;AAAA,QAAA;AAAA,MACH,GACF;AAAA,MAEF,oBAAC,UAAO,SAAQ,aAAY,MAAK,MAAK,0BAAO,aAAA,EAAY,GAAI,UAAU,QAAQ,GAAG,SAAS,MAAM,aAAa,OAAO,CAAC,GACnH,YAAE,MACL;AAAA,MACA,qBAAC,QAAA,EAAO,SAAQ,aAAY,MAAK,MAAK,UAAU,QAAQ,cAAc,SAAS,MAAM,aAAa,OAAO,CAAC,GACvG,UAAA;AAAA,QAAA,EAAE;AAAA,4BACF,cAAA,CAAA,CAAa;AAAA,MAAA,EAAA,CAChB;AAAA,IAAA,EAAA,CACF;AAAA,EAAA,GACF;AAEJ;"}
1
+ {"version":3,"file":"Pagination.js","sources":["../../../../../../src/v1/components/data/Pagination/Pagination.tsx"],"sourcesContent":["import React from 'react'\r\nimport Button from '../../../primitives/buttons/Button/Button'\r\nimport SelectInput from '../../../primitives/forms/SelectInput/SelectInput'\r\n\r\nexport interface PaginationLabels {\r\n /** \"{page}\" e \"{pages}\" são substituídos (ex: \"Página {page} de {pages}\"). */\r\n page: string\r\n /** \"{count}\" é substituído pelo total de registos (ex: \"{count} resultados\"). */\r\n total: string\r\n perPage: string\r\n prev: string\r\n next: string\r\n}\r\n\r\nexport const DEFAULT_PAGINATION_LABELS: PaginationLabels = {\r\n page: 'Page {page} of {pages}',\r\n total: '{count} results',\r\n perPage: 'Per page',\r\n prev: 'Previous',\r\n next: 'Next',\r\n}\r\n\r\nexport interface PaginationProps {\r\n /** Página atual, a contar de 1. */\r\n page: number\r\n /** Número de páginas. */\r\n pages: number\r\n onPageChange: (page: number) => void\r\n /** Total de registos (mostrado à esquerda). */\r\n total?: number\r\n /** Registos por página e as opções do seletor (sem opções, não há seletor). */\r\n pageSize?: number\r\n pageSizeOptions?: number[]\r\n onPageSizeChange?: (size: number) => void\r\n labels?: Partial<PaginationLabels>\r\n className?: string\r\n}\r\n\r\nfunction ChevronLeft() {\r\n return (\r\n <svg aria-hidden viewBox=\"0 0 24 24\" width={13} height={13} fill=\"none\" stroke=\"currentColor\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\">\r\n <path d=\"m15 18-6-6 6-6\" />\r\n </svg>\r\n )\r\n}\r\n\r\nfunction ChevronRight() {\r\n return (\r\n <svg aria-hidden viewBox=\"0 0 24 24\" width={12} height={12} fill=\"none\" stroke=\"currentColor\" strokeWidth={2} strokeLinecap=\"round\" strokeLinejoin=\"round\">\r\n <path d=\"m9 18 6-6-6-6\" />\r\n </svg>\r\n )\r\n}\r\n\r\n/**\r\n * Paginação por baixo de uma lista: \"Página 1 de 4\", o total, quantos por página e\r\n * Anterior/Seguinte. É a mesma que a `DataTable` mostra; usar esta quando a app pagina\r\n * no servidor (a tabela recebe só a página atual).\r\n */\r\nexport default function Pagination({\r\n page,\r\n pages,\r\n onPageChange,\r\n total,\r\n pageSize,\r\n pageSizeOptions,\r\n onPageSizeChange,\r\n labels,\r\n className = '',\r\n}: PaginationProps) {\r\n const L = { ...DEFAULT_PAGINATION_LABELS, ...labels }\r\n const totalPaginas = Math.max(1, pages)\r\n const opcoes = pageSizeOptions && pageSize != null ? [...new Set([...pageSizeOptions, pageSize])].sort((a, b) => a - b) : []\r\n return (\r\n // No telemóvel numa só linha: o texto à esquerda e as setas à direita, só com o ícone (com o texto\r\n // \"Anterior\"/\"Seguinte\" partia-se em duas linhas encostadas à esquerda); sem o seletor por página.\r\n <nav aria-label={L.page.replace('{page}', String(page)).replace('{pages}', String(totalPaginas))} className={`flex flex-wrap items-center justify-between gap-2 max-sm:flex-nowrap ${className}`}>\r\n <span className=\"min-w-0 text-[10px] font-black uppercase tracking-widest text-muted-foreground max-sm:tracking-wider\">\r\n {total != null && <>{L.total.replace('{count}', total.toLocaleString())} · </>}\r\n {L.page.replace('{page}', String(page)).replace('{pages}', String(totalPaginas))}\r\n </span>\r\n <div className=\"flex flex-wrap items-center gap-1.5 max-sm:shrink-0 max-sm:flex-nowrap\">\r\n {opcoes.length > 0 && onPageSizeChange && (\r\n <label className=\"mr-2 flex items-center gap-1.5 text-[10px] font-black uppercase tracking-widest text-muted-foreground max-sm:hidden\">\r\n {L.perPage}\r\n <SelectInput\r\n aria-label={L.perPage}\r\n value={String(pageSize)}\r\n onChange={(e) => onPageSizeChange(Number(e.target.value))}\r\n className=\"w-20 normal-case tracking-normal font-normal\"\r\n >\r\n {opcoes.map((n) => (\r\n <option key={n} value={n}>\r\n {n}\r\n </option>\r\n ))}\r\n </SelectInput>\r\n </label>\r\n )}\r\n <Button variant=\"secondary\" size=\"sm\" icon={<ChevronLeft />} disabled={page <= 1} onClick={() => onPageChange(page - 1)}>\r\n <span className=\"max-sm:sr-only\">{L.prev}</span>\r\n </Button>\r\n <Button variant=\"secondary\" size=\"sm\" disabled={page >= totalPaginas} onClick={() => onPageChange(page + 1)}>\r\n <span className=\"max-sm:sr-only\">{L.next}</span>\r\n <ChevronRight />\r\n </Button>\r\n </div>\r\n </nav>\r\n )\r\n}\r\n\r\nexport { Pagination }\r\n"],"names":[],"mappings":"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"}
@@ -9,11 +9,12 @@ function FormField({ label, required, hint, marks, markSlots, aside, end, wide,
9
9
  const { columns } = useFormGrid();
10
10
  const marksCount = React.Children.count(marks);
11
11
  const slots = Math.max(markSlots ?? 0, marksCount);
12
+ const wideRow = wide && columns === 2;
12
13
  return /* @__PURE__ */ jsxs(FieldContext.Provider, { value: { id, required }, children: [
13
14
  /* @__PURE__ */ jsxs(
14
15
  "div",
15
16
  {
16
- className: `flex flex-col items-end pr-3 text-right @max-form-sm:items-start @max-form-sm:pr-0 @max-form-sm:pt-0 @max-form-sm:text-left ${alignTop ? "self-start pt-[7px]" : ""}`,
17
+ className: `flex flex-col items-end pr-3 text-right @max-form-sm:items-start @max-form-sm:pr-0 @max-form-sm:pt-0 @max-form-sm:text-left ${wideRow ? "col-start-1" : ""} ${alignTop ? "self-start pt-[7px]" : ""}`,
17
18
  children: [
18
19
  /* @__PURE__ */ jsxs("div", { className: "flex items-center justify-end @max-form-sm:justify-start", children: [
19
20
  slots > 0 && /* @__PURE__ */ jsx("span", { className: "mr-[5px] inline-flex flex-none items-center justify-end gap-1", style: { width: slots * MARK + (slots - 1) * MARK_GAP }, children: marks }),
@@ -34,7 +35,7 @@ function FormField({ label, required, hint, marks, markSlots, aside, end, wide,
34
35
  ]
35
36
  }
36
37
  ),
37
- /* @__PURE__ */ jsxs("div", { className: `min-w-0 ${wide && columns === 2 ? "col-span-3 @max-form-sm:col-span-1" : ""} ${end ? "flex items-center gap-1.5" : ""}`, children: [
38
+ /* @__PURE__ */ jsxs("div", { className: `min-w-0 ${wideRow ? "col-span-3 @max-form-sm:col-span-1" : ""} ${end ? "flex items-center gap-1.5" : ""}`, children: [
38
39
  children,
39
40
  end
40
41
  ] })
@@ -1 +1 @@
1
- {"version":3,"file":"FormField.js","sources":["../../../../../../src/v1/components/forms/FormField/FormField.tsx"],"sourcesContent":["import React, { useId } from 'react'\r\nimport { FieldContext } from '../../../primitives/forms/field'\r\nimport { useFormGrid } from '../FormGrid/FormGrid'\r\n\r\nexport interface FormFieldProps {\r\n label: string\r\n /** Asterisco vermelho na etiqueta; o campo lá dentro fica `required`. */\r\n required?: boolean\r\n /** Texto pequeno por baixo da etiqueta. */\r\n hint?: string\r\n /**\r\n * Marcas antes da etiqueta, a dizer de onde veio o valor (ex: lido do QR,\r\n * identificado pela IA). Usar [FieldMark](#fieldmark).\r\n */\r\n marks?: React.ReactNode\r\n /**\r\n * Lugares reservados para marcas, mesmo sem nenhuma. Assim a etiqueta não\r\n * salta quando uma marca aparece depois (ex: ao ler o QR).\r\n */\r\n markSlots?: number\r\n /** Algo junto à etiqueta, depois das marcas (ex: o valor máximo permitido). */\r\n aside?: React.ReactNode\r\n /** Depois do campo, na mesma linha (ex: cadeado, ícone de informação). */\r\n end?: React.ReactNode\r\n /** Ocupa a linha toda (ex: observações, anexo). */\r\n wide?: boolean\r\n /** Etiqueta alinhada ao topo em vez de ao meio (campos altos, como o [TextArea](?path=/docs/v1-primitives-forms-textarea--docs)). */\r\n alignTop?: boolean\r\n /** O campo: [TextInput](?path=/docs/v1-primitives-forms-textinput--docs), [SelectInput](?path=/docs/v1-primitives-forms-selectinput--docs), [TextArea](?path=/docs/v1-primitives-forms-textarea--docs), [LookupInput](?path=/docs/v1-primitives-forms-lookupinput--docs) ou outro. */\r\n children: React.ReactNode\r\n}\r\n\r\nconst MARK = 17\r\nconst MARK_GAP = 4\r\n\r\n/**\r\n * Uma linha do formulário: etiqueta e campo. Vai dentro de um\r\n * [FormGrid](?path=/docs/v1-components-forms-formgrid--docs).\r\n *\r\n * Liga a etiqueta ao campo (clicar na etiqueta foca o campo) e passa-lhe o\r\n * `required`, se o campo for [TextInput](?path=/docs/v1-primitives-forms-textinput--docs), [SelectInput](?path=/docs/v1-primitives-forms-selectinput--docs), [TextArea](?path=/docs/v1-primitives-forms-textarea--docs) ou [LookupInput](?path=/docs/v1-primitives-forms-lookupinput--docs).\r\n */\r\nexport default function FormField({ label, required, hint, marks, markSlots, aside, end, wide, alignTop, children }: FormFieldProps) {\r\n const id = useId()\r\n const { columns } = useFormGrid()\r\n const marksCount = React.Children.count(marks)\r\n const slots = Math.max(markSlots ?? 0, marksCount)\r\n\r\n return (\r\n <FieldContext.Provider value={{ id, required }}>\r\n <div\r\n className={`flex flex-col items-end pr-3 text-right @max-form-sm:items-start @max-form-sm:pr-0 @max-form-sm:pt-0 @max-form-sm:text-left ${alignTop ? 'self-start pt-[7px]' : ''}`}\r\n >\r\n {/* As marcas ficam fora do <label>, para não entrarem no nome do campo lido pelos leitores de ecrã. */}\r\n <div className=\"flex items-center justify-end @max-form-sm:justify-start\">\r\n {slots > 0 && (\r\n <span className=\"mr-[5px] inline-flex flex-none items-center justify-end gap-1\" style={{ width: slots * MARK + (slots - 1) * MARK_GAP }}>\r\n {marks}\r\n </span>\r\n )}\r\n {aside && <span className=\"mr-1.5 flex-none\">{aside}</span>}\r\n <label\r\n htmlFor={id}\r\n className=\"m-0 text-xs leading-[18px] font-medium whitespace-nowrap text-muted-foreground @max-form-md:whitespace-normal\"\r\n >\r\n {label}\r\n {required && (\r\n <span aria-hidden className=\"ml-0.5 text-destructive\">\r\n *\r\n </span>\r\n )}\r\n </label>\r\n </div>\r\n {hint && <span className=\"text-micro text-muted-foreground\">{hint}</span>}\r\n </div>\r\n <div className={`min-w-0 ${wide && columns === 2 ? 'col-span-3 @max-form-sm:col-span-1' : ''} ${end ? 'flex items-center gap-1.5' : ''}`}>\r\n {children}\r\n {end}\r\n </div>\r\n </FieldContext.Provider>\r\n )\r\n}\r\n\r\nexport interface FieldMarkProps {\r\n /** De onde veio o valor (ex: \"Lido do QR Code\"). Mostrado ao passar o rato e lido pelos leitores de ecrã. */\r\n title: string\r\n /** O ícone, a 11 px (ex: `<QrCodeIcon size={11} />`). */\r\n icon: React.ReactNode\r\n}\r\n\r\n/** Marca pequena antes da etiqueta de um FormField, a dizer de onde veio o valor. */\r\nexport function FieldMark({ title, icon }: FieldMarkProps) {\r\n return (\r\n <span\r\n role=\"img\"\r\n aria-label={title}\r\n title={title}\r\n className=\"inline-flex h-[17px] w-[17px] flex-none cursor-help items-center justify-center rounded-[5px] border border-primary/20 bg-primary/5 text-primary\"\r\n >\r\n {icon}\r\n </span>\r\n )\r\n}\r\n"],"names":[],"mappings":";;;;AAgCA,MAAM,OAAO;AACb,MAAM,WAAW;AASjB,SAAwB,UAAU,EAAE,OAAO,UAAU,MAAM,OAAO,WAAW,OAAO,KAAK,MAAM,UAAU,SAAA,GAA4B;AACnI,QAAM,KAAK,MAAA;AACX,QAAM,EAAE,QAAA,IAAY,YAAA;AACpB,QAAM,aAAa,MAAM,SAAS,MAAM,KAAK;AAC7C,QAAM,QAAQ,KAAK,IAAI,aAAa,GAAG,UAAU;AAEjD,SACE,qBAAC,aAAa,UAAb,EAAsB,OAAO,EAAE,IAAI,YAClC,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAW,+HAA+H,WAAW,wBAAwB,EAAE;AAAA,QAG/K,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,4DACZ,UAAA;AAAA,YAAA,QAAQ,KACP,oBAAC,QAAA,EAAK,WAAU,iEAAgE,OAAO,EAAE,OAAO,QAAQ,QAAQ,QAAQ,KAAK,YAC1H,UAAA,OACH;AAAA,YAED,SAAS,oBAAC,QAAA,EAAK,WAAU,oBAAoB,UAAA,OAAM;AAAA,YACpD;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,SAAS;AAAA,gBACT,WAAU;AAAA,gBAET,UAAA;AAAA,kBAAA;AAAA,kBACA,YACC,oBAAC,QAAA,EAAK,eAAW,MAAC,WAAU,2BAA0B,UAAA,IAAA,CAEtD;AAAA,gBAAA;AAAA,cAAA;AAAA,YAAA;AAAA,UAEJ,GACF;AAAA,UACC,QAAQ,oBAAC,QAAA,EAAK,WAAU,oCAAoC,UAAA,KAAA,CAAK;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAEpE,qBAAC,OAAA,EAAI,WAAW,WAAW,QAAQ,YAAY,IAAI,uCAAuC,EAAE,IAAI,MAAM,8BAA8B,EAAE,IACnI,UAAA;AAAA,MAAA;AAAA,MACA;AAAA,IAAA,EAAA,CACH;AAAA,EAAA,GACF;AAEJ;AAUO,SAAS,UAAU,EAAE,OAAO,QAAwB;AACzD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ;AAAA,MACA,WAAU;AAAA,MAET,UAAA;AAAA,IAAA;AAAA,EAAA;AAGP;"}
1
+ {"version":3,"file":"FormField.js","sources":["../../../../../../src/v1/components/forms/FormField/FormField.tsx"],"sourcesContent":["import React, { useId } from 'react'\r\nimport { FieldContext } from '../../../primitives/forms/field'\r\nimport { useFormGrid } from '../FormGrid/FormGrid'\r\n\r\nexport interface FormFieldProps {\r\n label: string\r\n /** Asterisco vermelho na etiqueta; o campo lá dentro fica `required`. */\r\n required?: boolean\r\n /** Texto pequeno por baixo da etiqueta. */\r\n hint?: string\r\n /**\r\n * Marcas antes da etiqueta, a dizer de onde veio o valor (ex: lido do QR,\r\n * identificado pela IA). Usar [FieldMark](#fieldmark).\r\n */\r\n marks?: React.ReactNode\r\n /**\r\n * Lugares reservados para marcas, mesmo sem nenhuma. Assim a etiqueta não\r\n * salta quando uma marca aparece depois (ex: ao ler o QR).\r\n */\r\n markSlots?: number\r\n /** Algo junto à etiqueta, depois das marcas (ex: o valor máximo permitido). */\r\n aside?: React.ReactNode\r\n /** Depois do campo, na mesma linha (ex: cadeado, ícone de informação). */\r\n end?: React.ReactNode\r\n /** Ocupa a linha toda (ex: observações, anexo). */\r\n wide?: boolean\r\n /** Etiqueta alinhada ao topo em vez de ao meio (campos altos, como o [TextArea](?path=/docs/v1-primitives-forms-textarea--docs)). */\r\n alignTop?: boolean\r\n /** O campo: [TextInput](?path=/docs/v1-primitives-forms-textinput--docs), [SelectInput](?path=/docs/v1-primitives-forms-selectinput--docs), [TextArea](?path=/docs/v1-primitives-forms-textarea--docs), [LookupInput](?path=/docs/v1-primitives-forms-lookupinput--docs) ou outro. */\r\n children: React.ReactNode\r\n}\r\n\r\nconst MARK = 17\r\nconst MARK_GAP = 4\r\n\r\n/**\r\n * Uma linha do formulário: etiqueta e campo. Vai dentro de um\r\n * [FormGrid](?path=/docs/v1-components-forms-formgrid--docs).\r\n *\r\n * Liga a etiqueta ao campo (clicar na etiqueta foca o campo) e passa-lhe o\r\n * `required`, se o campo for [TextInput](?path=/docs/v1-primitives-forms-textinput--docs), [SelectInput](?path=/docs/v1-primitives-forms-selectinput--docs), [TextArea](?path=/docs/v1-primitives-forms-textarea--docs) ou [LookupInput](?path=/docs/v1-primitives-forms-lookupinput--docs).\r\n */\r\nexport default function FormField({ label, required, hint, marks, markSlots, aside, end, wide, alignTop, children }: FormFieldProps) {\r\n const id = useId()\r\n const { columns } = useFormGrid()\r\n const marksCount = React.Children.count(marks)\r\n const slots = Math.max(markSlots ?? 0, marksCount)\r\n // Em 2 colunas, o campo largo começa sempre numa linha nova: depois de um campo\r\n // de meia linha, a etiqueta caía na 3.ª coluna e o campo descia para a linha seguinte.\r\n const wideRow = wide && columns === 2\r\n\r\n return (\r\n <FieldContext.Provider value={{ id, required }}>\r\n <div\r\n className={`flex flex-col items-end pr-3 text-right @max-form-sm:items-start @max-form-sm:pr-0 @max-form-sm:pt-0 @max-form-sm:text-left ${wideRow ? 'col-start-1' : ''} ${alignTop ? 'self-start pt-[7px]' : ''}`}\r\n >\r\n {/* As marcas ficam fora do <label>, para não entrarem no nome do campo lido pelos leitores de ecrã. */}\r\n <div className=\"flex items-center justify-end @max-form-sm:justify-start\">\r\n {slots > 0 && (\r\n <span className=\"mr-[5px] inline-flex flex-none items-center justify-end gap-1\" style={{ width: slots * MARK + (slots - 1) * MARK_GAP }}>\r\n {marks}\r\n </span>\r\n )}\r\n {aside && <span className=\"mr-1.5 flex-none\">{aside}</span>}\r\n <label\r\n htmlFor={id}\r\n className=\"m-0 text-xs leading-[18px] font-medium whitespace-nowrap text-muted-foreground @max-form-md:whitespace-normal\"\r\n >\r\n {label}\r\n {required && (\r\n <span aria-hidden className=\"ml-0.5 text-destructive\">\r\n *\r\n </span>\r\n )}\r\n </label>\r\n </div>\r\n {hint && <span className=\"text-micro text-muted-foreground\">{hint}</span>}\r\n </div>\r\n <div className={`min-w-0 ${wideRow ? 'col-span-3 @max-form-sm:col-span-1' : ''} ${end ? 'flex items-center gap-1.5' : ''}`}>\r\n {children}\r\n {end}\r\n </div>\r\n </FieldContext.Provider>\r\n )\r\n}\r\n\r\nexport interface FieldMarkProps {\r\n /** De onde veio o valor (ex: \"Lido do QR Code\"). Mostrado ao passar o rato e lido pelos leitores de ecrã. */\r\n title: string\r\n /** O ícone, a 11 px (ex: `<QrCodeIcon size={11} />`). */\r\n icon: React.ReactNode\r\n}\r\n\r\n/** Marca pequena antes da etiqueta de um FormField, a dizer de onde veio o valor. */\r\nexport function FieldMark({ title, icon }: FieldMarkProps) {\r\n return (\r\n <span\r\n role=\"img\"\r\n aria-label={title}\r\n title={title}\r\n className=\"inline-flex h-[17px] w-[17px] flex-none cursor-help items-center justify-center rounded-[5px] border border-primary/20 bg-primary/5 text-primary\"\r\n >\r\n {icon}\r\n </span>\r\n )\r\n}\r\n"],"names":[],"mappings":";;;;AAgCA,MAAM,OAAO;AACb,MAAM,WAAW;AASjB,SAAwB,UAAU,EAAE,OAAO,UAAU,MAAM,OAAO,WAAW,OAAO,KAAK,MAAM,UAAU,SAAA,GAA4B;AACnI,QAAM,KAAK,MAAA;AACX,QAAM,EAAE,QAAA,IAAY,YAAA;AACpB,QAAM,aAAa,MAAM,SAAS,MAAM,KAAK;AAC7C,QAAM,QAAQ,KAAK,IAAI,aAAa,GAAG,UAAU;AAGjD,QAAM,UAAU,QAAQ,YAAY;AAEpC,SACE,qBAAC,aAAa,UAAb,EAAsB,OAAO,EAAE,IAAI,YAClC,UAAA;AAAA,IAAA;AAAA,MAAC;AAAA,MAAA;AAAA,QACC,WAAW,+HAA+H,UAAU,gBAAgB,EAAE,IAAI,WAAW,wBAAwB,EAAE;AAAA,QAG/M,UAAA;AAAA,UAAA,qBAAC,OAAA,EAAI,WAAU,4DACZ,UAAA;AAAA,YAAA,QAAQ,KACP,oBAAC,QAAA,EAAK,WAAU,iEAAgE,OAAO,EAAE,OAAO,QAAQ,QAAQ,QAAQ,KAAK,YAC1H,UAAA,OACH;AAAA,YAED,SAAS,oBAAC,QAAA,EAAK,WAAU,oBAAoB,UAAA,OAAM;AAAA,YACpD;AAAA,cAAC;AAAA,cAAA;AAAA,gBACC,SAAS;AAAA,gBACT,WAAU;AAAA,gBAET,UAAA;AAAA,kBAAA;AAAA,kBACA,YACC,oBAAC,QAAA,EAAK,eAAW,MAAC,WAAU,2BAA0B,UAAA,IAAA,CAEtD;AAAA,gBAAA;AAAA,cAAA;AAAA,YAAA;AAAA,UAEJ,GACF;AAAA,UACC,QAAQ,oBAAC,QAAA,EAAK,WAAU,oCAAoC,UAAA,KAAA,CAAK;AAAA,QAAA;AAAA,MAAA;AAAA,IAAA;AAAA,IAEpE,qBAAC,OAAA,EAAI,WAAW,WAAW,UAAU,uCAAuC,EAAE,IAAI,MAAM,8BAA8B,EAAE,IACrH,UAAA;AAAA,MAAA;AAAA,MACA;AAAA,IAAA,EAAA,CACH;AAAA,EAAA,GACF;AAEJ;AAUO,SAAS,UAAU,EAAE,OAAO,QAAwB;AACzD,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC,MAAK;AAAA,MACL,cAAY;AAAA,MACZ;AAAA,MACA,WAAU;AAAA,MAET,UAAA;AAAA,IAAA;AAAA,EAAA;AAGP;"}
@@ -65,8 +65,8 @@ function AppShell({
65
65
  type: "button",
66
66
  onClick: () => setOpen(true),
67
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",
68
+ style: { top: "calc(var(--app-shell-top) + 0.75rem)" },
69
+ className: "absolute 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
70
  children: /* @__PURE__ */ jsx(MenuIcon, { size: 20 })
71
71
  }
72
72
  ),
@@ -74,7 +74,12 @@ function AppShell({
74
74
  /* @__PURE__ */ jsx(
75
75
  "main",
76
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"),
77
+ className: (
78
+ // overflow-x-clip: uma peça mais larga do que o ecrã (um botão que não parte, um código comprido) fazia o
79
+ // telemóvel alargar a página toda e cortar tudo à direita. Assim só essa peça fica cortada. Clip, e não
80
+ // hidden, para não criar uma caixa de scroll (o sticky dentro da página continua a funcionar).
81
+ `min-w-0 overflow-x-clip px-page-x-narrow transition-[margin] duration-300 sm:px-page-x ${margin} ${topBar ? "pt-6" : "pt-page-top-narrow lg:pt-page-top max-lg:[--app-shell-title-lift:calc(var(--spacing-page-top-narrow)-19px)] max-lg:[--app-shell-menu-inset:calc(78px-var(--spacing-page-x-narrow))] sm:max-lg:[--app-shell-menu-inset:calc(78px-var(--spacing-page-x))]"} ` + (fullHeight ? "flex min-h-0 flex-1 flex-col overflow-hidden pb-4" : "pb-16")
82
+ ),
78
83
  children: /* @__PURE__ */ jsx(NavTrailContext.Provider, { value: trail, children })
79
84
  }
80
85
  )
@@ -1 +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;"}
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) + 0.75rem)' }}\r\n // absolute (não fixed): fica ao lado do título e sobe com a página; fixo ficava por cima do conteúdo ao deslizar.\r\n className=\"absolute 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 {/* Sem topBar, abaixo de 1024px o título do PageHeader sobe para o lado do botão\r\n do menu (as variáveis dizem-lhe quanto subir e quanto se afastar do botão). */}\r\n <main\r\n className={\r\n // overflow-x-clip: uma peça mais larga do que o ecrã (um botão que não parte, um código comprido) fazia o\r\n // telemóvel alargar a página toda e cortar tudo à direita. Assim só essa peça fica cortada. Clip, e não\r\n // hidden, para não criar uma caixa de scroll (o sticky dentro da página continua a funcionar).\r\n `min-w-0 overflow-x-clip px-page-x-narrow transition-[margin] duration-300 sm:px-page-x ${margin} ${topBar ? 'pt-6' : 'pt-page-top-narrow lg:pt-page-top max-lg:[--app-shell-title-lift:calc(var(--spacing-page-top-narrow)-19px)] max-lg:[--app-shell-menu-inset:calc(78px-var(--spacing-page-x-narrow))] sm:max-lg:[--app-shell-menu-inset:calc(78px-var(--spacing-page-x))]'} ` +\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":"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"}
@@ -7,7 +7,7 @@ function Page({ tabs, fill = false, children, breadcrumb, ...header }) {
7
7
  return /* @__PURE__ */ jsxs("div", { className: `flex flex-col gap-4 ${fill ? "min-h-0 flex-1" : ""}`, children: [
8
8
  /* @__PURE__ */ jsx(PageHeader, { ...header, breadcrumb: breadcrumb ?? auto }),
9
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 })
10
+ /* @__PURE__ */ jsx("div", { className: `${tabs ? "pt-2" : "pt-4 max-lg:pt-0"} ${fill ? "flex min-h-0 flex-1 flex-col overflow-hidden" : ""}`, children })
11
11
  ] });
12
12
  }
13
13
  export {
@@ -1 +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;"}
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 {/* Em ecrã estreito, menos espaço entre o cabeçalho e o conteúdo: no telemóvel cada linha conta. */}\r\n <div className={`${tabs ? 'pt-2' : 'pt-4 max-lg:pt-0'} ${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,IAE1C,oBAAC,OAAA,EAAI,WAAW,GAAG,OAAO,SAAS,kBAAkB,IAAI,OAAO,iDAAiD,EAAE,IAAK,SAAA,CAAS;AAAA,EAAA,GACnI;AAEJ;"}
@@ -1,24 +1,27 @@
1
1
  import { jsxs, jsx } from "react/jsx-runtime";
2
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 })
3
+ return (
4
+ // Dentro do AppShell, em ecrã estreito, o caminho e o título ficam ao lado do botão do menu.
5
+ /* @__PURE__ */ jsxs("header", { className: "flex flex-col gap-3 mt-[calc(var(--app-shell-title-lift,0px)*-1)]", children: [
6
+ breadcrumb && breadcrumb.length > 0 && /* @__PURE__ */ jsx("nav", { "aria-label": breadcrumbLabel, className: "pl-[var(--app-shell-menu-inset,0px)]", 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) => {
7
+ const last = i === breadcrumb.length - 1;
8
+ 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";
9
+ return /* @__PURE__ */ jsxs("li", { className: "flex items-center gap-1.5", children: [
10
+ 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 }),
11
+ !last && /* @__PURE__ */ jsx("span", { "aria-hidden": "true", className: "text-muted-foreground/60", children: "›" })
12
+ ] }, i);
13
+ }) }) }),
14
+ /* @__PURE__ */ jsxs("div", { className: "flex flex-col gap-4 lg:flex-row lg:items-end lg:justify-between", children: [
15
+ /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [
16
+ /* @__PURE__ */ jsx("h1", { className: "m-0 pl-[var(--app-shell-menu-inset,0px)] text-page-title font-black tracking-tighter text-primary uppercase dark:text-foreground", children: title }),
17
+ /* @__PURE__ */ jsx("div", { className: "mt-1 ml-[var(--app-shell-menu-inset,0px)] h-1 w-12 bg-proef-turquoise" }),
18
+ description && /* @__PURE__ */ jsx("p", { className: "m-0 mt-3 text-sm font-medium text-muted-foreground", children: description }),
19
+ extra && /* @__PURE__ */ jsx("div", { className: "mt-2 flex flex-wrap items-center gap-2 empty:hidden", children: extra })
20
+ ] }),
21
+ actions && /* @__PURE__ */ jsx("div", { className: "flex shrink-0 flex-wrap items-center gap-2 empty:hidden", children: actions })
22
+ ] })
20
23
  ] })
21
- ] });
24
+ );
22
25
  }
23
26
  export {
24
27
  PageHeader as default
@@ -1 +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;"}
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 // Dentro do AppShell, em ecrã estreito, o caminho e o título ficam ao lado do botão do menu.\r\n <header className=\"flex flex-col gap-3 mt-[calc(var(--app-shell-title-lift,0px)*-1)]\">\r\n {breadcrumb && breadcrumb.length > 0 && (\r\n <nav aria-label={breadcrumbLabel} className=\"pl-[var(--app-shell-menu-inset,0px)]\">\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 pl-[var(--app-shell-menu-inset,0px)] text-page-title font-black tracking-tighter text-primary uppercase dark:text-foreground\">{title}</h1>\r\n <div className=\"mt-1 ml-[var(--app-shell-menu-inset,0px)] 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;AAAA;AAAA,IAEE,qBAAC,UAAA,EAAO,WAAU,qEACf,UAAA;AAAA,MAAA,cAAc,WAAW,SAAS,KACjC,oBAAC,OAAA,EAAI,cAAY,iBAAiB,WAAU,wCAC1C,UAAA,oBAAC,QAAG,WAAU,qGACX,qBAAW,IAAI,CAAC,GAAG,MAAM;AACxB,cAAM,OAAO,MAAM,WAAW,SAAS;AACvC,cAAM,OAAO;AACb,eACE,qBAAC,MAAA,EAAW,WAAU,6BACnB,UAAA;AAAA,UAAA,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,UAEhB,CAAC,QACA,oBAAC,QAAA,EAAK,eAAY,QAAO,WAAU,4BAA2B,UAAA,IAAA,CAE9D;AAAA,QAAA,EAAA,GAnBK,CAqBT;AAAA,MAEJ,CAAC,GACH,GACF;AAAA,MAEF,qBAAC,OAAA,EAAI,WAAU,mEACb,UAAA;AAAA,QAAA,qBAAC,OAAA,EAAI,WAAU,WAEb,UAAA;AAAA,UAAA,oBAAC,MAAA,EAAG,WAAU,oIAAoI,UAAA,OAAM;AAAA,UACxJ,oBAAC,OAAA,EAAI,WAAU,wEAAA,CAAwE;AAAA,UACtF,eAAe,oBAAC,KAAA,EAAE,WAAU,sDAAsD,UAAA,aAAY;AAAA,UAG9F,SAAS,oBAAC,OAAA,EAAI,WAAU,uDAAuD,UAAA,MAAA,CAAM;AAAA,QAAA,GACxF;AAAA,QACC,WAAW,oBAAC,OAAA,EAAI,WAAU,2DAA2D,UAAA,QAAA,CAAQ;AAAA,MAAA,EAAA,CAChG;AAAA,IAAA,EAAA,CACF;AAAA;AAEJ;"}
@@ -19,11 +19,11 @@ function numerosPagina(atual, total) {
19
19
  }
20
20
  function LinhaEsqueleto({ marcavel, largura }) {
21
21
  return /* @__PURE__ */ jsxs("div", { className: `flex items-start gap-1.5 px-3 py-3 animate-pulse motion-reduce:animate-none ${LINHA_ESBATIDA}`, children: [
22
- marcavel && /* @__PURE__ */ jsx("span", { className: "flex w-7 shrink-0 justify-center", children: /* @__PURE__ */ jsx("span", { className: "mt-0.5 h-4 w-4 border border-slate-200 bg-slate-100" }) }),
22
+ marcavel && /* @__PURE__ */ jsx("span", { className: "flex w-7 shrink-0 justify-center", children: /* @__PURE__ */ jsx("span", { className: "mt-0.5 h-4 w-4 border border-slate-200 bg-slate-100 dark:border-slate-700 dark:bg-slate-800" }) }),
23
23
  /* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 space-y-1.5", children: [
24
- /* @__PURE__ */ jsx("div", { className: "h-3 w-20 bg-slate-200" }),
25
- /* @__PURE__ */ jsx("div", { className: `h-2.5 ${largura} bg-slate-200` }),
26
- /* @__PURE__ */ jsx("div", { className: "h-1.5 w-1/3 bg-slate-100" })
24
+ /* @__PURE__ */ jsx("div", { className: "h-3 w-20 bg-slate-200 dark:bg-slate-700" }),
25
+ /* @__PURE__ */ jsx("div", { className: `h-2.5 ${largura} bg-slate-200 dark:bg-slate-700` }),
26
+ /* @__PURE__ */ jsx("div", { className: "h-1.5 w-1/3 bg-slate-100 dark:bg-slate-800" })
27
27
  ] })
28
28
  ] });
29
29
  }
@@ -133,9 +133,9 @@ function ListFinder({
133
133
  onSelectionChange?.(n);
134
134
  };
135
135
  const aCarregar = loading && visiveis.length === 0;
136
- const esqueleto = /* @__PURE__ */ jsx("span", { className: "inline-block h-2 w-20 rounded-sm bg-slate-200 animate-pulse motion-reduce:animate-none", "aria-hidden": true });
137
- return /* @__PURE__ */ jsxs("div", { className: `v1-reset flex h-full min-h-0 flex-col bg-white text-control text-slate-800 ${bordered ? "border border-slate-200" : ""} ${className}`, children: [
138
- (searchable || onBack || filters) && /* @__PURE__ */ jsxs("div", { className: "space-y-2 border-b border-slate-100 px-3 py-2", children: [
136
+ const esqueleto = /* @__PURE__ */ jsx("span", { className: "inline-block h-2 w-20 rounded-sm bg-slate-200 dark:bg-slate-700 animate-pulse motion-reduce:animate-none", "aria-hidden": true });
137
+ return /* @__PURE__ */ jsxs("div", { className: `v1-reset flex h-full min-h-0 flex-col bg-white text-control text-slate-800 dark:bg-slate-900 dark:text-slate-100 ${bordered ? "border border-slate-200 dark:border-slate-700" : ""} ${className}`, children: [
138
+ (searchable || onBack || filters) && /* @__PURE__ */ jsxs("div", { className: "space-y-2 border-b border-slate-100 px-3 py-2 dark:border-slate-800", children: [
139
139
  (onBack || filters) && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5", children: [
140
140
  onBack && /* @__PURE__ */ jsx(
141
141
  "button",
@@ -144,7 +144,7 @@ function ListFinder({
144
144
  onClick: onBack,
145
145
  "aria-label": L.back,
146
146
  title: L.back,
147
- className: "-ml-1 flex h-7 w-6 shrink-0 items-center justify-center rounded text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700",
147
+ className: "-ml-1 flex h-7 w-6 shrink-0 items-center justify-center rounded text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200",
148
148
  children: /* @__PURE__ */ jsx(ChevronLeftIcon, { size: 14 })
149
149
  }
150
150
  ),
@@ -161,12 +161,12 @@ function ListFinder({
161
161
  onPaste: colar,
162
162
  placeholder: L.searchPlaceholder,
163
163
  "aria-label": L.search,
164
- className: "block w-full min-w-0 border border-slate-200 bg-white py-1.5 pr-2 pl-6 text-micro text-slate-700 outline-none transition-colors placeholder:text-slate-400 focus:border-proef-blue"
164
+ className: "block w-full min-w-0 border border-slate-200 bg-white py-1.5 pr-2 pl-6 text-micro text-slate-700 outline-none transition-colors placeholder:text-slate-400 focus:border-proef-blue dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100"
165
165
  }
166
166
  )
167
167
  ] })
168
168
  ] }),
169
- marcaveis && visiveis.length > 0 && /* @__PURE__ */ jsxs("div", { className: `flex items-center gap-1.5 bg-white px-3 py-2 text-[10px] text-slate-600 ${LINHA_ESBATIDA}`, children: [
169
+ marcaveis && visiveis.length > 0 && /* @__PURE__ */ jsxs("div", { className: `flex items-center gap-1.5 bg-white px-3 py-2 text-[10px] text-slate-600 dark:bg-slate-900 dark:text-slate-300 ${LINHA_ESBATIDA}`, children: [
170
170
  /* @__PURE__ */ jsx("span", { className: "flex w-7 shrink-0 justify-center", children: /* @__PURE__ */ jsx(
171
171
  Checkbox,
172
172
  {
@@ -219,19 +219,19 @@ function ListFinder({
219
219
  ] }),
220
220
  /* @__PURE__ */ jsx(OverlayScrollbar, { alvo: caixa, borda: 0 })
221
221
  ] }),
222
- /* @__PURE__ */ jsxs("div", { className: `flex shrink-0 items-center gap-2 border-t border-slate-100 bg-slate-50 px-3 text-micro text-slate-400 ${aRolar ? "h-8 justify-center" : "h-9 justify-between"}`, children: [
222
+ /* @__PURE__ */ jsxs("div", { className: `flex shrink-0 items-center gap-2 border-t border-slate-100 bg-slate-50 px-3 text-micro text-slate-400 dark:border-slate-800 dark:bg-slate-800/50 ${aRolar ? "h-8 justify-center" : "h-9 justify-between"}`, children: [
223
223
  /* @__PURE__ */ jsxs("span", { className: "flex items-center gap-1 whitespace-nowrap", children: [
224
224
  aCarregar ? esqueleto : aRolar ? (
225
225
  // A rolar: quantas já se mostram, de quantas há.
226
226
  /* @__PURE__ */ jsxs(Fragment, { children: [
227
- /* @__PURE__ */ jsx("span", { className: "font-medium text-slate-500 tabular-nums", children: visiveis.length.toLocaleString("pt-PT") }),
227
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-slate-500 tabular-nums dark:text-slate-300", children: visiveis.length.toLocaleString("pt-PT") }),
228
228
  " / ",
229
- /* @__PURE__ */ jsx("span", { className: "font-medium text-slate-500 tabular-nums", children: totalLinhas.toLocaleString("pt-PT") }),
229
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-slate-500 tabular-nums dark:text-slate-300", children: totalLinhas.toLocaleString("pt-PT") }),
230
230
  " ",
231
231
  L.itemsNoun(totalLinhas)
232
232
  ] })
233
233
  ) : /* @__PURE__ */ jsxs(Fragment, { children: [
234
- /* @__PURE__ */ jsx("span", { className: "font-medium text-slate-500 tabular-nums", children: totalLinhas.toLocaleString("pt-PT") }),
234
+ /* @__PURE__ */ jsx("span", { className: "font-medium text-slate-500 tabular-nums dark:text-slate-300", children: totalLinhas.toLocaleString("pt-PT") }),
235
235
  " ",
236
236
  L.itemsNoun(totalLinhas)
237
237
  ] }),
@@ -246,13 +246,13 @@ function ListFinder({
246
246
  " / ",
247
247
  totalPaginas
248
248
  ] }),
249
- numerosPagina(pagina, totalPaginas).map((p, i) => p === "…" ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "flex h-6 min-w-3 items-center justify-center text-slate-300", children: "…" }, `r${i}`) : /* @__PURE__ */ jsxs(
249
+ numerosPagina(pagina, totalPaginas).map((p, i) => p === "…" ? /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "flex h-6 min-w-3 items-center justify-center text-slate-300 dark:text-slate-600", children: "…" }, `r${i}`) : /* @__PURE__ */ jsxs(
250
250
  "button",
251
251
  {
252
252
  type: "button",
253
253
  onClick: () => irPara(p),
254
254
  "aria-current": p === pagina ? "page" : void 0,
255
- className: `grid h-6 min-w-5 place-items-center px-1 tabular-nums transition-colors ${p === pagina ? "cursor-default font-semibold text-slate-800" : "text-slate-400 hover:text-slate-700"}`,
255
+ className: `grid h-6 min-w-5 place-items-center px-1 tabular-nums transition-colors ${p === pagina ? "cursor-default font-semibold text-slate-800 dark:text-slate-100" : "text-slate-400 hover:text-slate-700 dark:hover:text-slate-200"}`,
256
256
  children: [
257
257
  /* @__PURE__ */ jsx("span", { "aria-hidden": true, className: "invisible col-start-1 row-start-1 font-semibold", children: p.toLocaleString("pt-PT") }),
258
258
  /* @__PURE__ */ jsx("span", { className: "col-start-1 row-start-1", children: p.toLocaleString("pt-PT") })
@@ -1 +1 @@
1
- {"version":3,"file":"ListFinder.js","sources":["../../../../../../src/v1/components/lists/ListFinder/ListFinder.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useState } from 'react'\r\nimport { ChevronLeftIcon, SearchIcon } from '../../../foundations/icons/icons'\r\nimport ListItem, { LINHA_ESBATIDA, type ListItemContent } from '../../../primitives/lists/ListItem/ListItem'\r\nimport Checkbox from '../../../primitives/forms/Checkbox/Checkbox'\r\nimport { OverlayScrollbar } from '../../tables/Table/OverlayScrollbar'\r\nimport { DEFAULT_LIST_FINDER_LABELS, type ListFinderLabels } from './labels'\r\n\r\nexport interface ListFinderProps<T> {\r\n /** As linhas: todas, ou só a página atual quando o servidor pagina (com `total`). */\r\n items: T[]\r\n /** Uma chave única por linha (ex: o id). */\r\n itemKey: (item: T) => string\r\n /** O que cada linha mostra: as partes do [ListItem](?path=/docs/v1-primitives-lists-listitem--docs). */\r\n itemContent: (item: T) => ListItemContent\r\n /** A linha aberta (fica realçada). */\r\n activeKey?: string | null\r\n /** Ao clicar numa linha: normalmente, abri-la ao lado. */\r\n onItemClick?: (item: T) => void\r\n /** As marcadas. Só há caixas de marcar com `onSelectionChange`. */\r\n selectedKeys?: ReadonlySet<string>\r\n onSelectionChange?: (keys: Set<string>) => void\r\n /** Filtros ao lado do voltar, por cima da pesquisa (ex: um ou dois selects, no traço fino da pesquisa). */\r\n filters?: React.ReactNode\r\n /** Com isto aparece um ‹ à esquerda, para fechar ou esconder a lista. */\r\n onBack?: () => void\r\n /** Com a caixa de pesquisa (o normal). Sem ela, a lista mostra tudo o que recebe. */\r\n searchable?: boolean\r\n /** O texto da pesquisa. Sem ele, a pesquisa guarda o seu. */\r\n search?: string\r\n onSearchChange?: (search: string) => void\r\n /**\r\n * O texto onde a pesquisa procura, em cada linha. Com isto, a lista filtra-se\r\n * sozinha (sem acentos nem maiúsculas); sem isto, quem filtra é a app (ex: o\r\n * servidor, com `onSearchChange`). Uma lista colada (uma por linha, ou\r\n * separadas por vírgulas) mostra as que tiverem qualquer uma delas.\r\n */\r\n searchText?: (item: T) => string\r\n /**\r\n * Como se chega ao resto da lista:\r\n * - `'pages'` (o normal): páginas numeradas no rodapé;\r\n * - `'scroll'`: uma lista só, que vai mostrando mais `pageSize` linhas ao\r\n * chegar ao fundo. Com o servidor a paginar, `items` são as que já vieram e\r\n * pede-se o lote seguinte com `onLoadMore`.\r\n */\r\n mode?: 'pages' | 'scroll'\r\n /** Com `mode=\"scroll\"` e o servidor a paginar: buscar o lote seguinte (junta-o a `items`). */\r\n onLoadMore?: () => void\r\n /** O servidor pagina: quantas linhas há lá ao todo. `items` é só a página atual (ou, a rolar, as que já vieram). */\r\n total?: number\r\n /** A página atual (1, 2, ...), quando o servidor pagina. */\r\n page?: number\r\n onPageChange?: (page: number) => void\r\n /** Quantas linhas por página. Com o servidor a paginar, quantas ele manda. */\r\n pageSize?: number\r\n /** A carregar: sem linhas ainda, mostra o esqueleto delas. A rolar, com linhas, é o lote seguinte a chegar. */\r\n loading?: boolean\r\n labels?: Partial<ListFinderLabels>\r\n /** No rodapé, a seguir à contagem: algo pequeno da app (ex: um ícone de ajuda com as regras da lista). */\r\n footerExtra?: React.ReactNode\r\n /** Com a moldura à volta (o normal). Sem ela, para ir dentro de um painel que já tem a sua. */\r\n bordered?: boolean\r\n className?: string\r\n}\r\n\r\nconst semAcentos = (s: string) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()\r\n\r\n/**\r\n * Parte o texto da pesquisa nos termos a procurar: vírgula, ponto e vírgula,\r\n * tab e mudança de linha. O espaço só separa quando tudo o que lá está são\r\n * números de 6 ou mais dígitos (uma coluna colada de uma folha de cálculo);\r\n * fora disso \"vila real\" é uma coisa só, e não \"vila\" ou \"real\".\r\n */\r\nexport function termosDaPesquisa(texto: string): string[] {\r\n const porEspacos = texto.trim().split(/\\s+/).filter(Boolean)\r\n if (porEspacos.length > 1 && porEspacos.every(t => /^\\d{6,}$/.test(t))) return porEspacos\r\n return texto.split(/[\\n,;\\t]+/).map(t => t.trim()).filter(Boolean)\r\n}\r\n\r\n/** Os números das páginas numa coluna estreita: no máximo cinco lugares. */\r\nfunction numerosPagina(atual: number, total: number): (number | '…')[] {\r\n if (total <= 5) return Array.from({ length: total }, (_, i) => i + 1)\r\n if (atual <= 2) return [1, 2, 3, '…', total]\r\n if (atual >= total - 1) return [1, '…', total - 2, total - 1, total]\r\n return [1, '…', atual, '…', total]\r\n}\r\n\r\n/** Uma linha a carregar: a forma das linhas já diz que vem ali a lista. */\r\nfunction LinhaEsqueleto({ marcavel, largura }: { marcavel: boolean; largura: string }) {\r\n return (\r\n <div className={`flex items-start gap-1.5 px-3 py-3 animate-pulse motion-reduce:animate-none ${LINHA_ESBATIDA}`}>\r\n {marcavel && <span className=\"flex w-7 shrink-0 justify-center\"><span className=\"mt-0.5 h-4 w-4 border border-slate-200 bg-slate-100\" /></span>}\r\n <div className=\"min-w-0 flex-1 space-y-1.5\">\r\n <div className=\"h-3 w-20 bg-slate-200\" />\r\n <div className={`h-2.5 ${largura} bg-slate-200`} />\r\n <div className=\"h-1.5 w-1/3 bg-slate-100\" />\r\n </div>\r\n </div>\r\n )\r\n}\r\n\r\n/**\r\n * Uma lista para encontrar e escolher: filtros e pesquisa em cima, as linhas\r\n * ([ListItem](?path=/docs/v1-primitives-lists-listitem--docs)) a rolar no meio e\r\n * as páginas (ou a contagem, a rolar) em baixo. Pensada para a coluna ao lado do que se abre (a lista à\r\n * esquerda, o detalhe da linha escolhida à direita).\r\n *\r\n * Enche a altura do pai. O topo, a barra de \"selecionar todos\" e o rodapé\r\n * ficam sempre no sítio: só as linhas rolam.\r\n *\r\n * - **Pesquisa**: com `searchText` filtra sozinha; sem ele, a app filtra\r\n * (`search`/`onSearchChange`).\r\n * - **Páginas**: pagina sozinha (`pageSize`), ou é o servidor (`total`, `page`,\r\n * `onPageChange`).\r\n * - **A rolar** (`mode=\"scroll\"`): sem páginas, vai mostrando mais ao descer;\r\n * com o servidor, `items` são as que já vieram e `onLoadMore` pede mais.\r\n * - **Realce**: o que se procurou realça-se no código e no título das linhas.\r\n * - **Marcar vários**: com `onSelectionChange`. O \"selecionar todos\" só marca\r\n * as linhas à vista, nunca as que não se veem.\r\n */\r\nexport default function ListFinder<T>({\r\n items, itemKey, itemContent, activeKey, onItemClick, selectedKeys, onSelectionChange, filters, onBack,\r\n searchable = true, search: pesquisaDeFora, onSearchChange, searchText, mode = 'pages', onLoadMore, total, page, onPageChange, pageSize = 50, loading = false,\r\n labels, footerExtra, bordered = true, className = '',\r\n}: ListFinderProps<T>) {\r\n const L = { ...DEFAULT_LIST_FINDER_LABELS, ...labels }\r\n const [pesquisaPropria, setPesquisaPropria] = useState('')\r\n const pesquisa = searchable ? (pesquisaDeFora ?? pesquisaPropria) : ''\r\n const pesquisar = (s: string) => { setPesquisaPropria(s); onSearchChange?.(s) }\r\n // Uma caixa de uma linha deita fora as quebras de linha: uma coluna colada\r\n // de uma folha de cálculo chegava como um número só, colado. Passam a vírgulas.\r\n const colar = (e: React.ClipboardEvent<HTMLInputElement>) => {\r\n const texto = e.clipboardData.getData('text')\r\n if (!/\\r?\\n/.test(texto.trim())) return\r\n e.preventDefault()\r\n const lista = texto.split(/\\r?\\n/).map(t => t.trim()).filter(Boolean).join(', ')\r\n const el = e.currentTarget\r\n pesquisar(el.value.slice(0, el.selectionStart ?? el.value.length) + lista + el.value.slice(el.selectionEnd ?? el.value.length))\r\n }\r\n\r\n // Os mesmos termos filtram (com `searchText`) e realçam-se nas linhas.\r\n const termos = useMemo(() => termosDaPesquisa(pesquisa).map(semAcentos), [pesquisa])\r\n const filtradas = useMemo(() => {\r\n if (!searchText || !termos.length) return items\r\n return items.filter(it => {\r\n const texto = semAcentos(searchText(it))\r\n return termos.some(t => texto.includes(t))\r\n })\r\n }, [items, termos, searchText])\r\n\r\n // Paginação: do servidor (com `total`), ou feita aqui.\r\n const peloServidor = total !== undefined\r\n const aRolar = mode === 'scroll'\r\n const [paginaPropria, setPaginaPropria] = useState(1)\r\n const totalLinhas = peloServidor ? total : filtradas.length\r\n const totalPaginas = Math.max(1, Math.ceil(totalLinhas / pageSize))\r\n const pagina = Math.min(peloServidor ? (page ?? 1) : paginaPropria, totalPaginas)\r\n // Uma pesquisa nova volta à primeira página: a terceira de antes já não é a mesma lista.\r\n useEffect(() => { if (!peloServidor) setPaginaPropria(1) }, [pesquisa, peloServidor])\r\n const irPara = (p: number) => { if (peloServidor) onPageChange?.(p); else setPaginaPropria(p) }\r\n // A rolar sem servidor: quantos lotes já se mostram.\r\n const [lotes, setLotes] = useState(1)\r\n useEffect(() => { setLotes(1) }, [pesquisa])\r\n const visiveis = peloServidor ? filtradas\r\n : aRolar ? filtradas.slice(0, lotes * pageSize)\r\n : filtradas.slice((pagina - 1) * pageSize, pagina * pageSize)\r\n const haMais = aRolar && (peloServidor ? !!onLoadMore && items.length < totalLinhas : visiveis.length < filtradas.length)\r\n\r\n // Ao mudar de página, ou de pesquisa a rolar, a lista volta ao topo.\r\n const [caixa, setCaixa] = useState<HTMLDivElement | null>(null)\r\n useEffect(() => { caixa?.scrollTo?.({ top: 0 }) }, [pagina, caixa])\r\n useEffect(() => { if (aRolar) caixa?.scrollTo?.({ top: 0 }) }, [pesquisa, aRolar, caixa])\r\n\r\n // A rolar: a linha-sentinela no fundo, ao chegar a 300px da vista, pede o\r\n // lote seguinte. Volta a observar a cada lote que chega: se continuar à\r\n // vista (uma janela alta), pede logo outro.\r\n const [fim, setFim] = useState<HTMLDivElement | null>(null)\r\n useEffect(() => {\r\n if (!fim || !caixa || !haMais || typeof IntersectionObserver === 'undefined') return\r\n const io = new IntersectionObserver(([e]) => {\r\n if (!e?.isIntersecting) return\r\n if (peloServidor) { if (!loading) onLoadMore?.() } else setLotes(n => n + 1)\r\n }, { root: caixa, rootMargin: '300px' })\r\n io.observe(fim)\r\n return () => io.disconnect()\r\n }, [fim, caixa, haMais, peloServidor, loading, onLoadMore, visiveis.length])\r\n\r\n const marcadas = selectedKeys ?? new Set<string>()\r\n const marcaveis = !!onSelectionChange\r\n const nMarcadasVisiveis = visiveis.filter(it => marcadas.has(itemKey(it))).length\r\n const todasMarcadas = visiveis.length > 0 && nMarcadasVisiveis === visiveis.length\r\n const marcar = (key: string, sim: boolean) => {\r\n const n = new Set(marcadas)\r\n if (sim) n.add(key); else n.delete(key)\r\n onSelectionChange?.(n)\r\n }\r\n const marcarVisiveis = () => {\r\n const n = new Set(marcadas)\r\n for (const it of visiveis) { if (todasMarcadas) n.delete(itemKey(it)); else n.add(itemKey(it)) }\r\n onSelectionChange?.(n)\r\n }\r\n\r\n const aCarregar = loading && visiveis.length === 0\r\n const esqueleto = <span className=\"inline-block h-2 w-20 rounded-sm bg-slate-200 animate-pulse motion-reduce:animate-none\" aria-hidden />\r\n\r\n return (\r\n <div className={`v1-reset flex h-full min-h-0 flex-col bg-white text-control text-slate-800 ${bordered ? 'border border-slate-200' : ''} ${className}`}>\r\n {/* Sem pesquisa, filtros nem voltar, não há cabeça: as linhas começam no topo. */}\r\n {(searchable || onBack || filters) && (\r\n <div className=\"space-y-2 border-b border-slate-100 px-3 py-2\">\r\n {(onBack || filters) && (\r\n <div className=\"flex items-center gap-1.5\">\r\n {onBack && (\r\n <button type=\"button\" onClick={onBack} aria-label={L.back} title={L.back}\r\n className=\"-ml-1 flex h-7 w-6 shrink-0 items-center justify-center rounded text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700\">\r\n <ChevronLeftIcon size={14} />\r\n </button>\r\n )}\r\n <div className=\"flex min-w-0 flex-1 items-center gap-1.5\">{filters}</div>\r\n </div>\r\n )}\r\n {searchable && (\r\n <div className=\"relative\">\r\n {/* Caixa direita e fina, no tamanho do texto das linhas: a lista é\r\n uma coluna estreita, e a pesquisa não deve pesar mais do que ela. */}\r\n <SearchIcon size={11} className=\"pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-slate-400\" />\r\n <input\r\n type=\"search\"\r\n value={pesquisa}\r\n onChange={e => pesquisar(e.target.value)}\r\n onPaste={colar}\r\n placeholder={L.searchPlaceholder}\r\n aria-label={L.search}\r\n className=\"block w-full min-w-0 border border-slate-200 bg-white py-1.5 pr-2 pl-6 text-micro text-slate-700 outline-none transition-colors placeholder:text-slate-400 focus:border-proef-blue\"\r\n />\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n\r\n {/* Fora da área que rola: é a cabeça da lista, e é a descer a lista que\r\n se decide marcá-la toda. Branca, com a linha esbatida das linhas por\r\n baixo: uma faixa cinzenta entre o branco da pesquisa e o das linhas\r\n era um tom quase igual a meio, e lia-se como sujidade. */}\r\n {marcaveis && visiveis.length > 0 && (\r\n <div className={`flex items-center gap-1.5 bg-white px-3 py-2 text-[10px] text-slate-600 ${LINHA_ESBATIDA}`}>\r\n {/* Na coluna de 28px das linhas, para as caixas ficarem umas por baixo das outras. */}\r\n <span className=\"flex w-7 shrink-0 justify-center\">\r\n <Checkbox\r\n checked={todasMarcadas}\r\n indeterminate={nMarcadasVisiveis > 0 && !todasMarcadas}\r\n onChange={marcarVisiveis}\r\n aria-label={L.selectVisible(visiveis.length)}\r\n />\r\n </span>\r\n <span aria-hidden>{nMarcadasVisiveis > 0 ? L.selectedCount(nMarcadasVisiveis, visiveis.length) : L.selectVisible(visiveis.length)}</span>\r\n {marcadas.size > 0 && (\r\n <button type=\"button\" onClick={() => onSelectionChange?.(new Set())}\r\n className=\"ml-auto text-proef-blue hover:underline\">\r\n {L.clearSelection}\r\n </button>\r\n )}\r\n </div>\r\n )}\r\n\r\n <div className=\"relative flex min-h-0 flex-1 flex-col\">\r\n {/* `relative`: o texto só para leitores de ecrã (`sr-only`, posição\r\n absoluta) do fundo da lista tem de ficar preso a esta caixa. Sem isso\r\n ficava no fim das linhas, fora do corte do scroll, e esticava a\r\n página — dava para rolar até um branco sem nada. */}\r\n <div ref={setCaixa} className=\"relative min-h-0 flex-1 overflow-y-auto scrollbar-none\">\r\n {aCarregar ? (\r\n // A forma das linhas já diz que vem ali a lista. Mais do que cabem,\r\n // cortadas pela caixa: chega ao fundo em qualquer altura.\r\n <div role=\"status\" aria-busy=\"true\" className=\"h-full overflow-hidden\">\r\n <span className=\"sr-only\">{L.loading}</span>\r\n {['w-11/12', 'w-3/4', 'w-5/6', 'w-2/3', 'w-4/5', 'w-7/12'].flatMap(l => [l, l, l, l]).map((largura, i) => (\r\n <LinhaEsqueleto key={i} marcavel={marcaveis} largura={largura} />\r\n ))}\r\n </div>\r\n ) : visiveis.length === 0 ? (\r\n <p className=\"m-0 px-3 py-4 text-center text-[10px] text-slate-400\">{L.empty}</p>\r\n ) : (\r\n <ul className=\"m-0 list-none p-0\">\r\n {visiveis.map(it => {\r\n const key = itemKey(it)\r\n return (\r\n <ListItem\r\n key={key}\r\n {...itemContent(it)}\r\n active={activeKey === key}\r\n onClick={onItemClick ? () => onItemClick(it) : undefined}\r\n checked={marcadas.has(key)}\r\n onCheckedChange={marcaveis ? sim => marcar(key, sim) : undefined}\r\n checkboxLabel={L.checkItem(key)}\r\n highlight={termos}\r\n />\r\n )\r\n })}\r\n </ul>\r\n )}\r\n {/* A rolar: o lote seguinte a chegar, e a sentinela que o pede. */}\r\n {!aCarregar && haMais && (\r\n <div ref={setFim} role=\"status\" aria-busy=\"true\">\r\n <span className=\"sr-only\">{L.loading}</span>\r\n {['w-3/4', 'w-5/6', 'w-2/3'].map((largura, i) => <LinhaEsqueleto key={i} marcavel={marcaveis} largura={largura} />)}\r\n </div>\r\n )}\r\n </div>\r\n <OverlayScrollbar alvo={caixa} borda={0} />\r\n </div>\r\n\r\n {/* Sempre à vista e com a mesma altura, carregada ou não: aparecer do\r\n nada fazia saltar o fundo do painel. */}\r\n <div className={`flex shrink-0 items-center gap-2 border-t border-slate-100 bg-slate-50 px-3 text-micro text-slate-400 ${aRolar ? 'h-8 justify-center' : 'h-9 justify-between'}`}>\r\n <span className=\"flex items-center gap-1 whitespace-nowrap\">\r\n {aCarregar ? esqueleto : aRolar ? (\r\n // A rolar: quantas já se mostram, de quantas há.\r\n <><span className=\"font-medium text-slate-500 tabular-nums\">{visiveis.length.toLocaleString('pt-PT')}</span> / <span className=\"font-medium text-slate-500 tabular-nums\">{totalLinhas.toLocaleString('pt-PT')}</span> {L.itemsNoun(totalLinhas)}</>\r\n ) : <><span className=\"font-medium text-slate-500 tabular-nums\">{totalLinhas.toLocaleString('pt-PT')}</span> {L.itemsNoun(totalLinhas)}</>}\r\n {footerExtra}\r\n {/* Com linhas e a pedir outra vez (ex: outra página, ou a atualizar): um \"…\" a pulsar. */}\r\n {loading && !aCarregar && <span aria-hidden className=\"animate-pulse motion-reduce:animate-none\">…</span>}\r\n </span>\r\n {!aRolar && !aCarregar && totalPaginas > 1 && (\r\n <nav aria-label={L.pagination} className=\"flex items-center gap-0.5\">\r\n <span className=\"sr-only\">{L.page} {pagina} / {totalPaginas}</span>\r\n {numerosPagina(pagina, totalPaginas).map((p, i) => p === '…' ? (\r\n <span key={`r${i}`} aria-hidden className=\"flex h-6 min-w-3 items-center justify-center text-slate-300\">…</span>\r\n ) : (\r\n <button\r\n key={p}\r\n type=\"button\"\r\n onClick={() => irPara(p)}\r\n aria-current={p === pagina ? 'page' : undefined}\r\n className={`grid h-6 min-w-5 place-items-center px-1 tabular-nums transition-colors ${\r\n p === pagina ? 'cursor-default font-semibold text-slate-800' : 'text-slate-400 hover:text-slate-700'}`}\r\n >\r\n {/* Sem caixa nem fundo: a página atual é só o número mais forte.\r\n A cópia invisível a negrito guarda a largura, para os números\r\n não se mexerem ao trocar de página. */}\r\n <span aria-hidden className=\"invisible col-start-1 row-start-1 font-semibold\">{p.toLocaleString('pt-PT')}</span>\r\n <span className=\"col-start-1 row-start-1\">{p.toLocaleString('pt-PT')}</span>\r\n </button>\r\n ))}\r\n </nav>\r\n )}\r\n </div>\r\n </div>\r\n )\r\n}\r\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"ListFinder.js","sources":["../../../../../../src/v1/components/lists/ListFinder/ListFinder.tsx"],"sourcesContent":["import React, { useEffect, useMemo, useState } from 'react'\r\nimport { ChevronLeftIcon, SearchIcon } from '../../../foundations/icons/icons'\r\nimport ListItem, { LINHA_ESBATIDA, type ListItemContent } from '../../../primitives/lists/ListItem/ListItem'\r\nimport Checkbox from '../../../primitives/forms/Checkbox/Checkbox'\r\nimport { OverlayScrollbar } from '../../tables/Table/OverlayScrollbar'\r\nimport { DEFAULT_LIST_FINDER_LABELS, type ListFinderLabels } from './labels'\r\n\r\nexport interface ListFinderProps<T> {\r\n /** As linhas: todas, ou só a página atual quando o servidor pagina (com `total`). */\r\n items: T[]\r\n /** Uma chave única por linha (ex: o id). */\r\n itemKey: (item: T) => string\r\n /** O que cada linha mostra: as partes do [ListItem](?path=/docs/v1-primitives-lists-listitem--docs). */\r\n itemContent: (item: T) => ListItemContent\r\n /** A linha aberta (fica realçada). */\r\n activeKey?: string | null\r\n /** Ao clicar numa linha: normalmente, abri-la ao lado. */\r\n onItemClick?: (item: T) => void\r\n /** As marcadas. Só há caixas de marcar com `onSelectionChange`. */\r\n selectedKeys?: ReadonlySet<string>\r\n onSelectionChange?: (keys: Set<string>) => void\r\n /** Filtros ao lado do voltar, por cima da pesquisa (ex: um ou dois selects, no traço fino da pesquisa). */\r\n filters?: React.ReactNode\r\n /** Com isto aparece um ‹ à esquerda, para fechar ou esconder a lista. */\r\n onBack?: () => void\r\n /** Com a caixa de pesquisa (o normal). Sem ela, a lista mostra tudo o que recebe. */\r\n searchable?: boolean\r\n /** O texto da pesquisa. Sem ele, a pesquisa guarda o seu. */\r\n search?: string\r\n onSearchChange?: (search: string) => void\r\n /**\r\n * O texto onde a pesquisa procura, em cada linha. Com isto, a lista filtra-se\r\n * sozinha (sem acentos nem maiúsculas); sem isto, quem filtra é a app (ex: o\r\n * servidor, com `onSearchChange`). Uma lista colada (uma por linha, ou\r\n * separadas por vírgulas) mostra as que tiverem qualquer uma delas.\r\n */\r\n searchText?: (item: T) => string\r\n /**\r\n * Como se chega ao resto da lista:\r\n * - `'pages'` (o normal): páginas numeradas no rodapé;\r\n * - `'scroll'`: uma lista só, que vai mostrando mais `pageSize` linhas ao\r\n * chegar ao fundo. Com o servidor a paginar, `items` são as que já vieram e\r\n * pede-se o lote seguinte com `onLoadMore`.\r\n */\r\n mode?: 'pages' | 'scroll'\r\n /** Com `mode=\"scroll\"` e o servidor a paginar: buscar o lote seguinte (junta-o a `items`). */\r\n onLoadMore?: () => void\r\n /** O servidor pagina: quantas linhas há lá ao todo. `items` é só a página atual (ou, a rolar, as que já vieram). */\r\n total?: number\r\n /** A página atual (1, 2, ...), quando o servidor pagina. */\r\n page?: number\r\n onPageChange?: (page: number) => void\r\n /** Quantas linhas por página. Com o servidor a paginar, quantas ele manda. */\r\n pageSize?: number\r\n /** A carregar: sem linhas ainda, mostra o esqueleto delas. A rolar, com linhas, é o lote seguinte a chegar. */\r\n loading?: boolean\r\n labels?: Partial<ListFinderLabels>\r\n /** No rodapé, a seguir à contagem: algo pequeno da app (ex: um ícone de ajuda com as regras da lista). */\r\n footerExtra?: React.ReactNode\r\n /** Com a moldura à volta (o normal). Sem ela, para ir dentro de um painel que já tem a sua. */\r\n bordered?: boolean\r\n className?: string\r\n}\r\n\r\nconst semAcentos = (s: string) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()\r\n\r\n/**\r\n * Parte o texto da pesquisa nos termos a procurar: vírgula, ponto e vírgula,\r\n * tab e mudança de linha. O espaço só separa quando tudo o que lá está são\r\n * números de 6 ou mais dígitos (uma coluna colada de uma folha de cálculo);\r\n * fora disso \"vila real\" é uma coisa só, e não \"vila\" ou \"real\".\r\n */\r\nexport function termosDaPesquisa(texto: string): string[] {\r\n const porEspacos = texto.trim().split(/\\s+/).filter(Boolean)\r\n if (porEspacos.length > 1 && porEspacos.every(t => /^\\d{6,}$/.test(t))) return porEspacos\r\n return texto.split(/[\\n,;\\t]+/).map(t => t.trim()).filter(Boolean)\r\n}\r\n\r\n/** Os números das páginas numa coluna estreita: no máximo cinco lugares. */\r\nfunction numerosPagina(atual: number, total: number): (number | '…')[] {\r\n if (total <= 5) return Array.from({ length: total }, (_, i) => i + 1)\r\n if (atual <= 2) return [1, 2, 3, '…', total]\r\n if (atual >= total - 1) return [1, '…', total - 2, total - 1, total]\r\n return [1, '…', atual, '…', total]\r\n}\r\n\r\n/** Uma linha a carregar: a forma das linhas já diz que vem ali a lista. */\r\nfunction LinhaEsqueleto({ marcavel, largura }: { marcavel: boolean; largura: string }) {\r\n return (\r\n <div className={`flex items-start gap-1.5 px-3 py-3 animate-pulse motion-reduce:animate-none ${LINHA_ESBATIDA}`}>\r\n {marcavel && <span className=\"flex w-7 shrink-0 justify-center\"><span className=\"mt-0.5 h-4 w-4 border border-slate-200 bg-slate-100 dark:border-slate-700 dark:bg-slate-800\" /></span>}\r\n <div className=\"min-w-0 flex-1 space-y-1.5\">\r\n <div className=\"h-3 w-20 bg-slate-200 dark:bg-slate-700\" />\r\n <div className={`h-2.5 ${largura} bg-slate-200 dark:bg-slate-700`} />\r\n <div className=\"h-1.5 w-1/3 bg-slate-100 dark:bg-slate-800\" />\r\n </div>\r\n </div>\r\n )\r\n}\r\n\r\n/**\r\n * Uma lista para encontrar e escolher: filtros e pesquisa em cima, as linhas\r\n * ([ListItem](?path=/docs/v1-primitives-lists-listitem--docs)) a rolar no meio e\r\n * as páginas (ou a contagem, a rolar) em baixo. Pensada para a coluna ao lado do que se abre (a lista à\r\n * esquerda, o detalhe da linha escolhida à direita).\r\n *\r\n * Enche a altura do pai. O topo, a barra de \"selecionar todos\" e o rodapé\r\n * ficam sempre no sítio: só as linhas rolam.\r\n *\r\n * - **Pesquisa**: com `searchText` filtra sozinha; sem ele, a app filtra\r\n * (`search`/`onSearchChange`).\r\n * - **Páginas**: pagina sozinha (`pageSize`), ou é o servidor (`total`, `page`,\r\n * `onPageChange`).\r\n * - **A rolar** (`mode=\"scroll\"`): sem páginas, vai mostrando mais ao descer;\r\n * com o servidor, `items` são as que já vieram e `onLoadMore` pede mais.\r\n * - **Realce**: o que se procurou realça-se no código e no título das linhas.\r\n * - **Marcar vários**: com `onSelectionChange`. O \"selecionar todos\" só marca\r\n * as linhas à vista, nunca as que não se veem.\r\n */\r\nexport default function ListFinder<T>({\r\n items, itemKey, itemContent, activeKey, onItemClick, selectedKeys, onSelectionChange, filters, onBack,\r\n searchable = true, search: pesquisaDeFora, onSearchChange, searchText, mode = 'pages', onLoadMore, total, page, onPageChange, pageSize = 50, loading = false,\r\n labels, footerExtra, bordered = true, className = '',\r\n}: ListFinderProps<T>) {\r\n const L = { ...DEFAULT_LIST_FINDER_LABELS, ...labels }\r\n const [pesquisaPropria, setPesquisaPropria] = useState('')\r\n const pesquisa = searchable ? (pesquisaDeFora ?? pesquisaPropria) : ''\r\n const pesquisar = (s: string) => { setPesquisaPropria(s); onSearchChange?.(s) }\r\n // Uma caixa de uma linha deita fora as quebras de linha: uma coluna colada\r\n // de uma folha de cálculo chegava como um número só, colado. Passam a vírgulas.\r\n const colar = (e: React.ClipboardEvent<HTMLInputElement>) => {\r\n const texto = e.clipboardData.getData('text')\r\n if (!/\\r?\\n/.test(texto.trim())) return\r\n e.preventDefault()\r\n const lista = texto.split(/\\r?\\n/).map(t => t.trim()).filter(Boolean).join(', ')\r\n const el = e.currentTarget\r\n pesquisar(el.value.slice(0, el.selectionStart ?? el.value.length) + lista + el.value.slice(el.selectionEnd ?? el.value.length))\r\n }\r\n\r\n // Os mesmos termos filtram (com `searchText`) e realçam-se nas linhas.\r\n const termos = useMemo(() => termosDaPesquisa(pesquisa).map(semAcentos), [pesquisa])\r\n const filtradas = useMemo(() => {\r\n if (!searchText || !termos.length) return items\r\n return items.filter(it => {\r\n const texto = semAcentos(searchText(it))\r\n return termos.some(t => texto.includes(t))\r\n })\r\n }, [items, termos, searchText])\r\n\r\n // Paginação: do servidor (com `total`), ou feita aqui.\r\n const peloServidor = total !== undefined\r\n const aRolar = mode === 'scroll'\r\n const [paginaPropria, setPaginaPropria] = useState(1)\r\n const totalLinhas = peloServidor ? total : filtradas.length\r\n const totalPaginas = Math.max(1, Math.ceil(totalLinhas / pageSize))\r\n const pagina = Math.min(peloServidor ? (page ?? 1) : paginaPropria, totalPaginas)\r\n // Uma pesquisa nova volta à primeira página: a terceira de antes já não é a mesma lista.\r\n useEffect(() => { if (!peloServidor) setPaginaPropria(1) }, [pesquisa, peloServidor])\r\n const irPara = (p: number) => { if (peloServidor) onPageChange?.(p); else setPaginaPropria(p) }\r\n // A rolar sem servidor: quantos lotes já se mostram.\r\n const [lotes, setLotes] = useState(1)\r\n useEffect(() => { setLotes(1) }, [pesquisa])\r\n const visiveis = peloServidor ? filtradas\r\n : aRolar ? filtradas.slice(0, lotes * pageSize)\r\n : filtradas.slice((pagina - 1) * pageSize, pagina * pageSize)\r\n const haMais = aRolar && (peloServidor ? !!onLoadMore && items.length < totalLinhas : visiveis.length < filtradas.length)\r\n\r\n // Ao mudar de página, ou de pesquisa a rolar, a lista volta ao topo.\r\n const [caixa, setCaixa] = useState<HTMLDivElement | null>(null)\r\n useEffect(() => { caixa?.scrollTo?.({ top: 0 }) }, [pagina, caixa])\r\n useEffect(() => { if (aRolar) caixa?.scrollTo?.({ top: 0 }) }, [pesquisa, aRolar, caixa])\r\n\r\n // A rolar: a linha-sentinela no fundo, ao chegar a 300px da vista, pede o\r\n // lote seguinte. Volta a observar a cada lote que chega: se continuar à\r\n // vista (uma janela alta), pede logo outro.\r\n const [fim, setFim] = useState<HTMLDivElement | null>(null)\r\n useEffect(() => {\r\n if (!fim || !caixa || !haMais || typeof IntersectionObserver === 'undefined') return\r\n const io = new IntersectionObserver(([e]) => {\r\n if (!e?.isIntersecting) return\r\n if (peloServidor) { if (!loading) onLoadMore?.() } else setLotes(n => n + 1)\r\n }, { root: caixa, rootMargin: '300px' })\r\n io.observe(fim)\r\n return () => io.disconnect()\r\n }, [fim, caixa, haMais, peloServidor, loading, onLoadMore, visiveis.length])\r\n\r\n const marcadas = selectedKeys ?? new Set<string>()\r\n const marcaveis = !!onSelectionChange\r\n const nMarcadasVisiveis = visiveis.filter(it => marcadas.has(itemKey(it))).length\r\n const todasMarcadas = visiveis.length > 0 && nMarcadasVisiveis === visiveis.length\r\n const marcar = (key: string, sim: boolean) => {\r\n const n = new Set(marcadas)\r\n if (sim) n.add(key); else n.delete(key)\r\n onSelectionChange?.(n)\r\n }\r\n const marcarVisiveis = () => {\r\n const n = new Set(marcadas)\r\n for (const it of visiveis) { if (todasMarcadas) n.delete(itemKey(it)); else n.add(itemKey(it)) }\r\n onSelectionChange?.(n)\r\n }\r\n\r\n const aCarregar = loading && visiveis.length === 0\r\n const esqueleto = <span className=\"inline-block h-2 w-20 rounded-sm bg-slate-200 dark:bg-slate-700 animate-pulse motion-reduce:animate-none\" aria-hidden />\r\n\r\n return (\r\n <div className={`v1-reset flex h-full min-h-0 flex-col bg-white text-control text-slate-800 dark:bg-slate-900 dark:text-slate-100 ${bordered ? 'border border-slate-200 dark:border-slate-700' : ''} ${className}`}>\r\n {/* Sem pesquisa, filtros nem voltar, não há cabeça: as linhas começam no topo. */}\r\n {(searchable || onBack || filters) && (\r\n <div className=\"space-y-2 border-b border-slate-100 px-3 py-2 dark:border-slate-800\">\r\n {(onBack || filters) && (\r\n <div className=\"flex items-center gap-1.5\">\r\n {onBack && (\r\n <button type=\"button\" onClick={onBack} aria-label={L.back} title={L.back}\r\n className=\"-ml-1 flex h-7 w-6 shrink-0 items-center justify-center rounded text-slate-400 transition-colors hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800 dark:hover:text-slate-200\">\r\n <ChevronLeftIcon size={14} />\r\n </button>\r\n )}\r\n <div className=\"flex min-w-0 flex-1 items-center gap-1.5\">{filters}</div>\r\n </div>\r\n )}\r\n {searchable && (\r\n <div className=\"relative\">\r\n {/* Caixa direita e fina, no tamanho do texto das linhas: a lista é\r\n uma coluna estreita, e a pesquisa não deve pesar mais do que ela. */}\r\n <SearchIcon size={11} className=\"pointer-events-none absolute top-1/2 left-2 -translate-y-1/2 text-slate-400\" />\r\n <input\r\n type=\"search\"\r\n value={pesquisa}\r\n onChange={e => pesquisar(e.target.value)}\r\n onPaste={colar}\r\n placeholder={L.searchPlaceholder}\r\n aria-label={L.search}\r\n className=\"block w-full min-w-0 border border-slate-200 bg-white py-1.5 pr-2 pl-6 text-micro text-slate-700 outline-none transition-colors placeholder:text-slate-400 focus:border-proef-blue dark:border-slate-700 dark:bg-slate-800 dark:text-slate-100\"\r\n />\r\n </div>\r\n )}\r\n </div>\r\n )}\r\n\r\n {/* Fora da área que rola: é a cabeça da lista, e é a descer a lista que\r\n se decide marcá-la toda. Branca, com a linha esbatida das linhas por\r\n baixo: uma faixa cinzenta entre o branco da pesquisa e o das linhas\r\n era um tom quase igual a meio, e lia-se como sujidade. */}\r\n {marcaveis && visiveis.length > 0 && (\r\n <div className={`flex items-center gap-1.5 bg-white px-3 py-2 text-[10px] text-slate-600 dark:bg-slate-900 dark:text-slate-300 ${LINHA_ESBATIDA}`}>\r\n {/* Na coluna de 28px das linhas, para as caixas ficarem umas por baixo das outras. */}\r\n <span className=\"flex w-7 shrink-0 justify-center\">\r\n <Checkbox\r\n checked={todasMarcadas}\r\n indeterminate={nMarcadasVisiveis > 0 && !todasMarcadas}\r\n onChange={marcarVisiveis}\r\n aria-label={L.selectVisible(visiveis.length)}\r\n />\r\n </span>\r\n <span aria-hidden>{nMarcadasVisiveis > 0 ? L.selectedCount(nMarcadasVisiveis, visiveis.length) : L.selectVisible(visiveis.length)}</span>\r\n {marcadas.size > 0 && (\r\n <button type=\"button\" onClick={() => onSelectionChange?.(new Set())}\r\n className=\"ml-auto text-proef-blue hover:underline\">\r\n {L.clearSelection}\r\n </button>\r\n )}\r\n </div>\r\n )}\r\n\r\n <div className=\"relative flex min-h-0 flex-1 flex-col\">\r\n {/* `relative`: o texto só para leitores de ecrã (`sr-only`, posição\r\n absoluta) do fundo da lista tem de ficar preso a esta caixa. Sem isso\r\n ficava no fim das linhas, fora do corte do scroll, e esticava a\r\n página — dava para rolar até um branco sem nada. */}\r\n <div ref={setCaixa} className=\"relative min-h-0 flex-1 overflow-y-auto scrollbar-none\">\r\n {aCarregar ? (\r\n // A forma das linhas já diz que vem ali a lista. Mais do que cabem,\r\n // cortadas pela caixa: chega ao fundo em qualquer altura.\r\n <div role=\"status\" aria-busy=\"true\" className=\"h-full overflow-hidden\">\r\n <span className=\"sr-only\">{L.loading}</span>\r\n {['w-11/12', 'w-3/4', 'w-5/6', 'w-2/3', 'w-4/5', 'w-7/12'].flatMap(l => [l, l, l, l]).map((largura, i) => (\r\n <LinhaEsqueleto key={i} marcavel={marcaveis} largura={largura} />\r\n ))}\r\n </div>\r\n ) : visiveis.length === 0 ? (\r\n <p className=\"m-0 px-3 py-4 text-center text-[10px] text-slate-400\">{L.empty}</p>\r\n ) : (\r\n <ul className=\"m-0 list-none p-0\">\r\n {visiveis.map(it => {\r\n const key = itemKey(it)\r\n return (\r\n <ListItem\r\n key={key}\r\n {...itemContent(it)}\r\n active={activeKey === key}\r\n onClick={onItemClick ? () => onItemClick(it) : undefined}\r\n checked={marcadas.has(key)}\r\n onCheckedChange={marcaveis ? sim => marcar(key, sim) : undefined}\r\n checkboxLabel={L.checkItem(key)}\r\n highlight={termos}\r\n />\r\n )\r\n })}\r\n </ul>\r\n )}\r\n {/* A rolar: o lote seguinte a chegar, e a sentinela que o pede. */}\r\n {!aCarregar && haMais && (\r\n <div ref={setFim} role=\"status\" aria-busy=\"true\">\r\n <span className=\"sr-only\">{L.loading}</span>\r\n {['w-3/4', 'w-5/6', 'w-2/3'].map((largura, i) => <LinhaEsqueleto key={i} marcavel={marcaveis} largura={largura} />)}\r\n </div>\r\n )}\r\n </div>\r\n <OverlayScrollbar alvo={caixa} borda={0} />\r\n </div>\r\n\r\n {/* Sempre à vista e com a mesma altura, carregada ou não: aparecer do\r\n nada fazia saltar o fundo do painel. */}\r\n <div className={`flex shrink-0 items-center gap-2 border-t border-slate-100 bg-slate-50 px-3 text-micro text-slate-400 dark:border-slate-800 dark:bg-slate-800/50 ${aRolar ? 'h-8 justify-center' : 'h-9 justify-between'}`}>\r\n <span className=\"flex items-center gap-1 whitespace-nowrap\">\r\n {aCarregar ? esqueleto : aRolar ? (\r\n // A rolar: quantas já se mostram, de quantas há.\r\n <><span className=\"font-medium text-slate-500 tabular-nums dark:text-slate-300\">{visiveis.length.toLocaleString('pt-PT')}</span> / <span className=\"font-medium text-slate-500 tabular-nums dark:text-slate-300\">{totalLinhas.toLocaleString('pt-PT')}</span> {L.itemsNoun(totalLinhas)}</>\r\n ) : <><span className=\"font-medium text-slate-500 tabular-nums dark:text-slate-300\">{totalLinhas.toLocaleString('pt-PT')}</span> {L.itemsNoun(totalLinhas)}</>}\r\n {footerExtra}\r\n {/* Com linhas e a pedir outra vez (ex: outra página, ou a atualizar): um \"…\" a pulsar. */}\r\n {loading && !aCarregar && <span aria-hidden className=\"animate-pulse motion-reduce:animate-none\">…</span>}\r\n </span>\r\n {!aRolar && !aCarregar && totalPaginas > 1 && (\r\n <nav aria-label={L.pagination} className=\"flex items-center gap-0.5\">\r\n <span className=\"sr-only\">{L.page} {pagina} / {totalPaginas}</span>\r\n {numerosPagina(pagina, totalPaginas).map((p, i) => p === '…' ? (\r\n <span key={`r${i}`} aria-hidden className=\"flex h-6 min-w-3 items-center justify-center text-slate-300 dark:text-slate-600\">…</span>\r\n ) : (\r\n <button\r\n key={p}\r\n type=\"button\"\r\n onClick={() => irPara(p)}\r\n aria-current={p === pagina ? 'page' : undefined}\r\n className={`grid h-6 min-w-5 place-items-center px-1 tabular-nums transition-colors ${\r\n p === pagina ? 'cursor-default font-semibold text-slate-800 dark:text-slate-100' : 'text-slate-400 hover:text-slate-700 dark:hover:text-slate-200'}`}\r\n >\r\n {/* Sem caixa nem fundo: a página atual é só o número mais forte.\r\n A cópia invisível a negrito guarda a largura, para os números\r\n não se mexerem ao trocar de página. */}\r\n <span aria-hidden className=\"invisible col-start-1 row-start-1 font-semibold\">{p.toLocaleString('pt-PT')}</span>\r\n <span className=\"col-start-1 row-start-1\">{p.toLocaleString('pt-PT')}</span>\r\n </button>\r\n ))}\r\n </nav>\r\n )}\r\n </div>\r\n </div>\r\n )\r\n}\r\n"],"names":[],"mappings":"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"}
@@ -91,7 +91,7 @@ function Dialog({
91
91
  onClick: (e) => {
92
92
  if (closeOnBackdrop && pressedBackdrop.current && noFundo(e)) onClose();
93
93
  },
94
- className: `fixed inset-0 z-[200] items-center justify-center p-dialog-margin ${open ? "flex" : "hidden"}`,
94
+ className: `fixed inset-0 z-[200] items-center justify-center p-dialog-margin ${height === "full" ? "max-sm:p-0" : ""} ${open ? "flex" : "hidden"}`,
95
95
  children: [
96
96
  /* @__PURE__ */ jsx("div", { ref: fundo, className: "absolute inset-0 bg-black/40 motion-safe:animate-backdrop-in" }),
97
97
  /* @__PURE__ */ jsxs(
@@ -102,7 +102,10 @@ function Dialog({
102
102
  "aria-modal": "true",
103
103
  "aria-labelledby": titleId,
104
104
  tabIndex: -1,
105
- className: `relative flex max-h-full w-full ${WIDTHS[size]} flex-col overflow-hidden rounded-2xl bg-card text-foreground shadow-modal outline-none motion-safe:animate-dialog-in ${height === "full" ? "h-full" : ""}`,
105
+ className: (
106
+ // No escuro a sombra não se vê: a linha (a mesma dos menus) separa a janela do fundo.
107
+ `relative flex max-h-full w-full ${WIDTHS[size]} flex-col overflow-hidden rounded-2xl bg-card text-foreground shadow-modal outline-none dark:border dark:border-slate-700 motion-safe:animate-dialog-in ${height === "full" ? "h-full max-sm:rounded-none max-sm:border-0" : ""}`
108
+ ),
106
109
  children: [
107
110
  /* @__PURE__ */ jsxs("header", { className: "flex flex-none items-center justify-between gap-3 bg-gradient-to-br from-primary to-primary-strong px-5 py-3.5", children: [
108
111
  /* @__PURE__ */ jsxs("div", { className: "min-w-0", children: [