@nextelco/common-ui 2.1.105 → 2.1.107
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/index.js +201 -145
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/legacy/components/atoms/InputData/InputData.js +2 -13
- package/dist/esm/legacy/components/atoms/InputData/InputData.js.map +1 -1
- package/dist/esm/legacy/components/charts/Charts.js +147 -57
- package/dist/esm/legacy/components/charts/Charts.js.map +1 -1
- package/dist/esm/legacy/components/layout/Layout/Layout.js +21 -82
- package/dist/esm/legacy/components/layout/Layout/Layout.js.map +1 -1
- package/dist/esm/legacy/components/layout/Sidebar/Sidebar.js +29 -218
- package/dist/esm/legacy/components/layout/Sidebar/Sidebar.js.map +1 -1
- package/dist/esm/legacy/components/molecules/ColumnPicker/ColumnPicker.js +77 -10
- package/dist/esm/legacy/components/molecules/ColumnPicker/ColumnPicker.js.map +1 -1
- package/dist/esm/legacy/components/molecules/DataTable/DataTable.js +445 -337
- package/dist/esm/legacy/components/molecules/DataTable/DataTable.js.map +1 -1
- package/dist/esm/legacy/components/molecules/UserAvatar/UserAvatar.js +15 -97
- package/dist/esm/legacy/components/molecules/UserAvatar/UserAvatar.js.map +1 -1
- package/dist/esm/legacy/components/organisms/MapaPontos/MapaPontos.js +3 -3
- package/dist/esm/legacy/components/organisms/MapaPontos/MapaPontos.js.map +1 -1
- package/dist/esm/legacy/components/organisms/PainelFiltros/PainelFiltros.js +15 -3
- package/dist/esm/legacy/components/organisms/PainelFiltros/PainelFiltros.js.map +1 -1
- package/dist/esm/legacy/components/organisms/Selector/Selector.js +4 -4
- package/dist/esm/legacy/components/organisms/Selector/Selector.js.map +1 -1
- package/dist/esm/legacy/components/ui/Avisos.js +23 -76
- package/dist/esm/legacy/components/ui/Avisos.js.map +1 -1
- package/dist/esm/legacy/components/ui/CabecalhoPagina.js +14 -27
- package/dist/esm/legacy/components/ui/CabecalhoPagina.js.map +1 -1
- package/dist/esm/legacy/components/ui/CaixaPesquisa.js +3 -51
- package/dist/esm/legacy/components/ui/CaixaPesquisa.js.map +1 -1
- package/dist/esm/legacy/components/ui/Confirmacao.js +13 -31
- package/dist/esm/legacy/components/ui/Confirmacao.js.map +1 -1
- package/dist/esm/legacy/components/ui/Escolha.js +21 -137
- package/dist/esm/legacy/components/ui/Escolha.js.map +1 -1
- package/dist/esm/legacy/components/ui/FaixaAviso.js +13 -11
- package/dist/esm/legacy/components/ui/FaixaAviso.js.map +1 -1
- package/dist/esm/legacy/components/ui/Separadores.js +11 -46
- package/dist/esm/legacy/components/ui/Separadores.js.map +1 -1
- package/dist/esm/legacy/components/ui/Ui.js +18 -115
- package/dist/esm/legacy/components/ui/Ui.js.map +1 -1
- package/dist/esm/legacy/components/ui/painelFlutuante.js +5 -44
- package/dist/esm/legacy/components/ui/painelFlutuante.js.map +1 -1
- package/dist/esm/v1/capabilities/overlay/useFloatingPanel.js +52 -0
- package/dist/esm/v1/capabilities/overlay/useFloatingPanel.js.map +1 -0
- package/dist/esm/v1/capabilities/theme/useIsDark.js +15 -0
- package/dist/esm/v1/capabilities/theme/useIsDark.js.map +1 -0
- package/dist/esm/v1/capabilities/theme/useTheme.js +66 -0
- package/dist/esm/v1/capabilities/theme/useTheme.js.map +1 -0
- package/dist/esm/v1/components/data/Pagination/Pagination.js +66 -0
- package/dist/esm/v1/components/data/Pagination/Pagination.js.map +1 -0
- package/dist/esm/v1/components/feedback/Toast/Toast.js +105 -0
- package/dist/esm/v1/components/feedback/Toast/Toast.js.map +1 -0
- package/dist/esm/v1/components/forms/FormField/FormField.js.map +1 -1
- package/dist/esm/v1/components/forms/FormGrid/FormGrid.js.map +1 -1
- package/dist/esm/v1/components/layout/AppShell/AppShell.js +89 -0
- package/dist/esm/v1/components/layout/AppShell/AppShell.js.map +1 -0
- package/dist/esm/v1/components/layout/Page/Page.js +16 -0
- package/dist/esm/v1/components/layout/Page/Page.js.map +1 -0
- package/dist/esm/v1/components/layout/PageHeader/PageHeader.js +26 -0
- package/dist/esm/v1/components/layout/PageHeader/PageHeader.js.map +1 -0
- package/dist/esm/v1/components/layout/SideNav/SideNav.js +277 -0
- package/dist/esm/v1/components/layout/SideNav/SideNav.js.map +1 -0
- package/dist/esm/v1/components/layout/navTrail.js +19 -0
- package/dist/esm/v1/components/layout/navTrail.js.map +1 -0
- package/dist/esm/v1/components/lists/ListFinder/ListFinder.js +4 -4
- package/dist/esm/v1/components/lists/ListFinder/ListFinder.js.map +1 -1
- package/dist/esm/v1/components/overlays/ConfirmDialog/ConfirmDialog.js +95 -0
- package/dist/esm/v1/components/overlays/ConfirmDialog/ConfirmDialog.js.map +1 -0
- package/dist/esm/v1/components/overlays/Dialog/Dialog.js.map +1 -1
- package/dist/esm/v1/components/overlays/Tooltip/Tooltip.js +98 -0
- package/dist/esm/v1/components/overlays/Tooltip/Tooltip.js.map +1 -0
- package/dist/esm/v1/components/overlays/Tooltip/TooltipLayer.js +84 -0
- package/dist/esm/v1/components/overlays/Tooltip/TooltipLayer.js.map +1 -0
- package/dist/esm/v1/components/overlays/Tooltip/TooltipPanel.js +33 -0
- package/dist/esm/v1/components/overlays/Tooltip/TooltipPanel.js.map +1 -0
- package/dist/esm/v1/components/overlays/topLayer.js +28 -0
- package/dist/esm/v1/components/overlays/topLayer.js.map +1 -0
- package/dist/esm/v1/components/tables/PaginatedTable/PaginatedTable.js.map +1 -1
- package/dist/esm/v1/components/tables/Table/Table.js.map +1 -1
- package/dist/esm/v1/foundations/icons/icons.js +61 -2
- package/dist/esm/v1/foundations/icons/icons.js.map +1 -1
- package/dist/esm/v1/primitives/buttons/Button/Button.js +18 -4
- package/dist/esm/v1/primitives/buttons/Button/Button.js.map +1 -1
- package/dist/esm/v1/primitives/buttons/SegmentedControl/SegmentedControl.js +65 -0
- package/dist/esm/v1/primitives/buttons/SegmentedControl/SegmentedControl.js.map +1 -0
- package/dist/esm/v1/primitives/buttons/SegmentedControl/segmentedStyles.js +10 -0
- package/dist/esm/v1/primitives/buttons/SegmentedControl/segmentedStyles.js.map +1 -0
- package/dist/esm/v1/primitives/display/Avatar/Avatar.js +120 -0
- package/dist/esm/v1/primitives/display/Avatar/Avatar.js.map +1 -0
- package/dist/esm/v1/primitives/display/Badge/Badge.js +23 -0
- package/dist/esm/v1/primitives/display/Badge/Badge.js.map +1 -0
- package/dist/esm/v1/primitives/display/Card/Card.js +22 -0
- package/dist/esm/v1/primitives/display/Card/Card.js.map +1 -0
- package/dist/esm/v1/primitives/display/FilterChips/FilterChips.js +45 -0
- package/dist/esm/v1/primitives/display/FilterChips/FilterChips.js.map +1 -0
- package/dist/esm/v1/primitives/display/Kpi/Kpi.js +103 -0
- package/dist/esm/v1/primitives/display/Kpi/Kpi.js.map +1 -0
- package/dist/esm/v1/primitives/display/Notice/Notice.js +71 -0
- package/dist/esm/v1/primitives/display/Notice/Notice.js.map +1 -0
- package/dist/esm/v1/primitives/display/Person/Person.js +14 -0
- package/dist/esm/v1/primitives/display/Person/Person.js.map +1 -0
- package/dist/esm/v1/primitives/forms/Checkbox/Checkbox.js +10 -5
- package/dist/esm/v1/primitives/forms/Checkbox/Checkbox.js.map +1 -1
- package/dist/esm/v1/primitives/forms/DateInput/DateInput.js +150 -0
- package/dist/esm/v1/primitives/forms/DateInput/DateInput.js.map +1 -0
- package/dist/esm/v1/primitives/forms/LookupInput/LookupInput.js.map +1 -1
- package/dist/esm/v1/primitives/forms/SearchInput/SearchInput.js +43 -0
- package/dist/esm/v1/primitives/forms/SearchInput/SearchInput.js.map +1 -0
- package/dist/esm/v1/primitives/forms/SelectInput/SelectInput.js +190 -12
- package/dist/esm/v1/primitives/forms/SelectInput/SelectInput.js.map +1 -1
- package/dist/esm/v1/primitives/forms/TextArea/TextArea.js.map +1 -1
- package/dist/esm/v1/primitives/forms/field.js +4 -2
- package/dist/esm/v1/primitives/forms/field.js.map +1 -1
- package/dist/esm/v1/primitives/lists/ListItem/ListItem.js +2 -2
- package/dist/esm/v1/primitives/lists/ListItem/ListItem.js.map +1 -1
- package/dist/esm/v1/primitives/navigation/Tabs/Tabs.js +96 -0
- package/dist/esm/v1/primitives/navigation/Tabs/Tabs.js.map +1 -0
- package/dist/style.components.css +1 -1
- package/dist/style.css +1 -1
- package/dist/style.layered.css +1 -1
- package/dist/tailwind.css +24 -0
- package/dist/theme-boot.js +1 -0
- package/dist/theme.css +10 -3
- package/dist/tokens.css +119 -0
- package/dist/types/legacy/components/atoms/InputData/InputData.d.ts +4 -1
- package/dist/types/legacy/components/charts/Charts.d.ts +58 -5
- package/dist/types/legacy/components/layout/Layout/Layout.d.ts +7 -20
- package/dist/types/legacy/components/layout/Sidebar/Sidebar.d.ts +13 -26
- package/dist/types/legacy/components/molecules/ColumnPicker/ColumnPicker.d.ts +56 -3
- package/dist/types/legacy/components/molecules/DataTable/DataTable.d.ts +72 -7
- package/dist/types/legacy/components/molecules/UserAvatar/UserAvatar.d.ts +4 -5
- package/dist/types/legacy/components/ui/Avisos.d.ts +6 -16
- package/dist/types/legacy/components/ui/CabecalhoPagina.d.ts +6 -20
- package/dist/types/legacy/components/ui/CaixaPesquisa.d.ts +2 -4
- package/dist/types/legacy/components/ui/Confirmacao.d.ts +3 -10
- package/dist/types/legacy/components/ui/Escolha.d.ts +5 -15
- package/dist/types/legacy/components/ui/FaixaAviso.d.ts +2 -6
- package/dist/types/legacy/components/ui/Separadores.d.ts +3 -10
- package/dist/types/legacy/components/ui/Ui.d.ts +12 -54
- package/dist/types/legacy/components/ui/painelFlutuante.d.ts +6 -10
- package/dist/types/legacy/exports/atoms.d.ts +1 -1
- package/dist/types/legacy/exports/charts.d.ts +2 -2
- package/dist/types/legacy/exports/hooks.d.ts +2 -2
- package/dist/types/legacy/exports/molecules.d.ts +2 -2
- package/dist/types/v1/capabilities/overlay/useFloatingPanel.d.ts +20 -0
- package/dist/types/v1/capabilities/theme/useIsDark.d.ts +10 -0
- package/dist/types/v1/capabilities/theme/useTheme.d.ts +29 -0
- package/dist/types/v1/components/data/Pagination/Pagination.d.ts +33 -0
- package/dist/types/v1/components/feedback/Toast/Toast.d.ts +51 -0
- package/dist/types/v1/components/forms/FormField/FormField.d.ts +3 -3
- package/dist/types/v1/components/forms/FormGrid/FormGrid.d.ts +2 -0
- package/dist/types/v1/components/layout/AppShell/AppShell.d.ts +35 -0
- package/dist/types/v1/components/layout/Page/Page.d.ts +23 -0
- package/dist/types/v1/components/layout/PageHeader/PageHeader.d.ts +35 -0
- package/dist/types/v1/components/layout/SideNav/SideNav.d.ts +92 -0
- package/dist/types/v1/components/layout/navTrail.d.ts +15 -0
- package/dist/types/v1/components/lists/ListFinder/ListFinder.d.ts +1 -1
- package/dist/types/v1/components/overlays/ConfirmDialog/ConfirmDialog.d.ts +59 -0
- package/dist/types/v1/components/overlays/Dialog/Dialog.d.ts +6 -3
- package/dist/types/v1/components/overlays/Tooltip/Tooltip.d.ts +34 -0
- package/dist/types/v1/components/overlays/Tooltip/TooltipLayer.d.ts +18 -0
- package/dist/types/v1/components/overlays/Tooltip/TooltipPanel.d.ts +23 -0
- package/dist/types/v1/components/overlays/topLayer.d.ts +23 -0
- package/dist/types/v1/components/tables/PaginatedTable/PaginatedTable.d.ts +1 -1
- package/dist/types/v1/components/tables/Table/Table.d.ts +1 -1
- package/dist/types/v1/foundations/icons/icons.d.ts +20 -0
- package/dist/types/v1/index.d.ts +43 -1
- package/dist/types/v1/primitives/buttons/Button/Button.d.ts +22 -5
- package/dist/types/v1/primitives/buttons/SegmentedControl/SegmentedControl.d.ts +28 -0
- package/dist/types/v1/primitives/buttons/SegmentedControl/segmentedStyles.d.ts +16 -0
- package/dist/types/v1/primitives/display/Avatar/Avatar.d.ts +30 -0
- package/dist/types/v1/primitives/display/Badge/Badge.d.ts +25 -0
- package/dist/types/v1/primitives/display/Card/Card.d.ts +22 -0
- package/dist/types/v1/primitives/display/FilterChips/FilterChips.d.ts +33 -0
- package/dist/types/v1/primitives/display/Kpi/Kpi.d.ts +68 -0
- package/dist/types/v1/primitives/display/Notice/Notice.d.ts +29 -0
- package/dist/types/v1/primitives/display/Person/Person.d.ts +15 -0
- package/dist/types/v1/primitives/forms/Checkbox/Checkbox.d.ts +7 -0
- package/dist/types/v1/primitives/forms/DateInput/DateInput.d.ts +43 -0
- package/dist/types/v1/primitives/forms/LookupInput/LookupInput.d.ts +3 -0
- package/dist/types/v1/primitives/forms/SearchInput/SearchInput.d.ts +23 -0
- package/dist/types/v1/primitives/forms/SelectInput/SelectInput.d.ts +61 -10
- package/dist/types/v1/primitives/forms/TextArea/TextArea.d.ts +3 -0
- package/dist/types/v1/primitives/forms/field.d.ts +2 -0
- package/dist/types/v1/primitives/lists/ListItem/ListItem.d.ts +2 -1
- package/dist/types/v1/primitives/navigation/Tabs/Tabs.d.ts +58 -0
- package/package.json +3 -3
- package/dist/esm/legacy/components/atoms/Tooltip/Tooltip.css +0 -17
- package/dist/esm/legacy/components/atoms/Tooltip/Tooltip.js +0 -41
- package/dist/esm/legacy/components/atoms/Tooltip/Tooltip.js.map +0 -1
- package/dist/esm/legacy/components/molecules/Search/Search.css +0 -18
- package/dist/esm/legacy/components/molecules/Search/Search.js +0 -30
- package/dist/esm/legacy/components/molecules/Search/Search.js.map +0 -1
- package/dist/esm/legacy/hooks/useTheme.js +0 -39
- package/dist/esm/legacy/hooks/useTheme.js.map +0 -1
- package/dist/esm/v1/primitives/lists/ListItem/ListCheckbox.js +0 -28
- package/dist/esm/v1/primitives/lists/ListItem/ListCheckbox.js.map +0 -1
- package/dist/types/legacy/components/atoms/Tooltip/Tooltip.d.ts +0 -13
- package/dist/types/legacy/components/molecules/Search/Search.d.ts +0 -7
- package/dist/types/legacy/hooks/useTheme.d.ts +0 -17
- package/dist/types/v1/primitives/lists/ListItem/ListCheckbox.d.ts +0 -14
|
@@ -2,7 +2,7 @@ import { jsxs, jsx, Fragment } from "react/jsx-runtime";
|
|
|
2
2
|
import { useState, useMemo, useEffect } from "react";
|
|
3
3
|
import { ChevronLeftIcon, SearchIcon } from "../../../foundations/icons/icons.js";
|
|
4
4
|
import ListItem from "../../../primitives/lists/ListItem/ListItem.js";
|
|
5
|
-
import
|
|
5
|
+
import Checkbox from "../../../primitives/forms/Checkbox/Checkbox.js";
|
|
6
6
|
import { OverlayScrollbar } from "../../tables/Table/OverlayScrollbar.js";
|
|
7
7
|
import { DEFAULT_LIST_FINDER_LABELS } from "./labels.js";
|
|
8
8
|
const semAcentos = (s) => s.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
|
|
@@ -127,12 +127,12 @@ function ListFinder({
|
|
|
127
127
|
] }),
|
|
128
128
|
marcaveis && visiveis.length > 0 && /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-2.5 border-b border-slate-100 bg-slate-50 px-3 py-1.5 text-caption text-slate-500", children: [
|
|
129
129
|
/* @__PURE__ */ jsx(
|
|
130
|
-
|
|
130
|
+
Checkbox,
|
|
131
131
|
{
|
|
132
132
|
checked: todasMarcadas,
|
|
133
|
-
indeterminate: nMarcadasVisiveis > 0,
|
|
133
|
+
indeterminate: nMarcadasVisiveis > 0 && !todasMarcadas,
|
|
134
134
|
onChange: marcarVisiveis,
|
|
135
|
-
label: L.selectVisible(visiveis.length)
|
|
135
|
+
"aria-label": L.selectVisible(visiveis.length)
|
|
136
136
|
}
|
|
137
137
|
),
|
|
138
138
|
/* @__PURE__ */ jsx("span", { "aria-hidden": true, children: nMarcadasVisiveis > 0 ? L.selectedCount(nMarcadasVisiveis, visiveis.length) : L.selectVisible(visiveis.length) }),
|
|
@@ -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, { type ListItemContent } from '../../../primitives/lists/ListItem/ListItem'\r\nimport { ListCheckbox } from '../../../primitives/lists/ListItem/ListCheckbox'\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 SelectInput). */\r\n filters?: React.ReactNode\r\n /** Com isto aparece um ‹ à esquerda, para fechar ou esconder a lista. */\r\n onBack?: () => void\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 /** O servidor pagina: quantas linhas há lá ao todo. `items` é só a página atual. */\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. */\r\n loading?: boolean\r\n labels?: Partial<ListFinderLabels>\r\n className?: string\r\n}\r\n\r\nconst semAcentos = (s: string) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()\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/**\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 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 * - **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 search: pesquisaDeFora, onSearchChange, searchText, total, page, onPageChange, pageSize = 50, loading = false,\r\n labels, className = '',\r\n}: ListFinderProps<T>) {\r\n const L = { ...DEFAULT_LIST_FINDER_LABELS, ...labels }\r\n const [pesquisaPropria, setPesquisaPropria] = useState('')\r\n const pesquisa = 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 const filtradas = useMemo(() => {\r\n if (!searchText || !pesquisa.trim()) return items\r\n const termos = pesquisa.split(/[\\n,;\\t]+/).map(t => semAcentos(t.trim())).filter(Boolean)\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, pesquisa, searchText])\r\n\r\n // Paginação: do servidor (com `total`), ou feita aqui.\r\n const peloServidor = total !== undefined\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 const visiveis = peloServidor ? filtradas : filtradas.slice((pagina - 1) * pageSize, pagina * pageSize)\r\n\r\n // Ao mudar de página, 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\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 border border-slate-200 bg-white text-control text-slate-800 ${className}`}>\r\n <div className=\"space-y-2 border-b border-slate-100 px-3 py-2.5\">\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 <div className=\"relative\">\r\n <SearchIcon size={13} className=\"pointer-events-none absolute top-1/2 left-2.5 -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 h-control w-full min-w-0 rounded-lg border border-border bg-card pr-2.5 pl-8 text-control text-foreground outline-none transition-[border-color,box-shadow] duration-150 placeholder:text-muted-foreground/70 focus:border-primary/40 focus:ring-3 focus:ring-primary/10\"\r\n />\r\n </div>\r\n </div>\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. */}\r\n {marcaveis && visiveis.length > 0 && (\r\n <div className=\"flex items-center gap-2.5 border-b border-slate-100 bg-slate-50 px-3 py-1.5 text-caption text-slate-500\">\r\n <ListCheckbox\r\n checked={todasMarcadas}\r\n indeterminate={nMarcadasVisiveis > 0}\r\n onChange={marcarVisiveis}\r\n label={L.selectVisible(visiveis.length)}\r\n />\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 font-medium 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 <div ref={setCaixa} className=\"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 <div key={i} className=\"flex items-start gap-2.5 border-b border-slate-100 px-3 py-2.5 animate-pulse motion-reduce:animate-none\">\r\n {marcaveis && <div className=\"mt-0.5 h-4 w-4 shrink-0 border border-slate-200 bg-slate-100\" />}\r\n <div className=\"min-w-0 flex-1 space-y-1.5\">\r\n <div className=\"h-3 w-12 bg-slate-200\" />\r\n <div className=\"h-1.5 w-16 bg-slate-100\" />\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 </div>\r\n ) : visiveis.length === 0 ? (\r\n <p className=\"m-0 px-3 py-6 text-center text-caption 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 />\r\n )\r\n })}\r\n </ul>\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 h-11 shrink-0 items-center justify-between gap-2 border-t border-slate-100 bg-slate-50 px-3 text-caption text-slate-500\">\r\n <span className=\"whitespace-nowrap\">\r\n {aCarregar ? esqueleto : <><span className=\"font-semibold text-slate-800 tabular-nums\">{totalLinhas.toLocaleString('pt-PT')}</span> {L.itemsNoun(totalLinhas)}</>}\r\n </span>\r\n {!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-7 min-w-4 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={`h-7 min-w-7 rounded-md px-1.5 font-semibold tabular-nums transition-colors ${\r\n p === pagina ? 'cursor-default bg-primary text-white' : 'text-slate-500 hover:bg-slate-200/70 hover:text-slate-800'}`}\r\n >\r\n {p.toLocaleString('pt-PT')}\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, { 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 [SelectInput](?path=/docs/v1-primitives-forms-selectinput--docs)). */\r\n filters?: React.ReactNode\r\n /** Com isto aparece um ‹ à esquerda, para fechar ou esconder a lista. */\r\n onBack?: () => void\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 /** O servidor pagina: quantas linhas há lá ao todo. `items` é só a página atual. */\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. */\r\n loading?: boolean\r\n labels?: Partial<ListFinderLabels>\r\n className?: string\r\n}\r\n\r\nconst semAcentos = (s: string) => s.normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase()\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/**\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 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 * - **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 search: pesquisaDeFora, onSearchChange, searchText, total, page, onPageChange, pageSize = 50, loading = false,\r\n labels, className = '',\r\n}: ListFinderProps<T>) {\r\n const L = { ...DEFAULT_LIST_FINDER_LABELS, ...labels }\r\n const [pesquisaPropria, setPesquisaPropria] = useState('')\r\n const pesquisa = 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 const filtradas = useMemo(() => {\r\n if (!searchText || !pesquisa.trim()) return items\r\n const termos = pesquisa.split(/[\\n,;\\t]+/).map(t => semAcentos(t.trim())).filter(Boolean)\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, pesquisa, searchText])\r\n\r\n // Paginação: do servidor (com `total`), ou feita aqui.\r\n const peloServidor = total !== undefined\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 const visiveis = peloServidor ? filtradas : filtradas.slice((pagina - 1) * pageSize, pagina * pageSize)\r\n\r\n // Ao mudar de página, 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\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 border border-slate-200 bg-white text-control text-slate-800 ${className}`}>\r\n <div className=\"space-y-2 border-b border-slate-100 px-3 py-2.5\">\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 <div className=\"relative\">\r\n <SearchIcon size={13} className=\"pointer-events-none absolute top-1/2 left-2.5 -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 h-control w-full min-w-0 rounded-lg border border-border bg-card pr-2.5 pl-8 text-control text-foreground outline-none transition-[border-color,box-shadow] duration-150 placeholder:text-muted-foreground/70 focus:border-primary/40 focus:ring-3 focus:ring-primary/10\"\r\n />\r\n </div>\r\n </div>\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. */}\r\n {marcaveis && visiveis.length > 0 && (\r\n <div className=\"flex items-center gap-2.5 border-b border-slate-100 bg-slate-50 px-3 py-1.5 text-caption text-slate-500\">\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 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 font-medium 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 <div ref={setCaixa} className=\"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 <div key={i} className=\"flex items-start gap-2.5 border-b border-slate-100 px-3 py-2.5 animate-pulse motion-reduce:animate-none\">\r\n {marcaveis && <div className=\"mt-0.5 h-4 w-4 shrink-0 border border-slate-200 bg-slate-100\" />}\r\n <div className=\"min-w-0 flex-1 space-y-1.5\">\r\n <div className=\"h-3 w-12 bg-slate-200\" />\r\n <div className=\"h-1.5 w-16 bg-slate-100\" />\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 </div>\r\n ) : visiveis.length === 0 ? (\r\n <p className=\"m-0 px-3 py-6 text-center text-caption 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 />\r\n )\r\n })}\r\n </ul>\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 h-11 shrink-0 items-center justify-between gap-2 border-t border-slate-100 bg-slate-50 px-3 text-caption text-slate-500\">\r\n <span className=\"whitespace-nowrap\">\r\n {aCarregar ? esqueleto : <><span className=\"font-semibold text-slate-800 tabular-nums\">{totalLinhas.toLocaleString('pt-PT')}</span> {L.itemsNoun(totalLinhas)}</>}\r\n </span>\r\n {!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-7 min-w-4 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={`h-7 min-w-7 rounded-md px-1.5 font-semibold tabular-nums transition-colors ${\r\n p === pagina ? 'cursor-default bg-primary text-white' : 'text-slate-500 hover:bg-slate-200/70 hover:text-slate-800'}`}\r\n >\r\n {p.toLocaleString('pt-PT')}\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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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { jsx, jsxs, Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useRef, useEffect, useState, useCallback, useContext, createContext } from "react";
|
|
3
|
+
import Dialog from "../Dialog/Dialog.js";
|
|
4
|
+
import Button from "../../../primitives/buttons/Button/Button.js";
|
|
5
|
+
import { WarningIcon } from "../../../foundations/icons/icons.js";
|
|
6
|
+
function ConfirmDialog({
|
|
7
|
+
open,
|
|
8
|
+
title,
|
|
9
|
+
message,
|
|
10
|
+
detail,
|
|
11
|
+
tone = "default",
|
|
12
|
+
confirmLabel = "Confirmar",
|
|
13
|
+
cancelLabel = "Cancelar",
|
|
14
|
+
closeLabel = "Fechar",
|
|
15
|
+
loading = false,
|
|
16
|
+
secondaryAction,
|
|
17
|
+
onConfirm,
|
|
18
|
+
onCancel
|
|
19
|
+
}) {
|
|
20
|
+
const danger = tone === "danger";
|
|
21
|
+
const confirmRef = useRef(null);
|
|
22
|
+
const cancelRef = useRef(null);
|
|
23
|
+
useEffect(() => {
|
|
24
|
+
if (open) (danger ? cancelRef : confirmRef).current?.focus();
|
|
25
|
+
}, [open, danger]);
|
|
26
|
+
return /* @__PURE__ */ jsx(
|
|
27
|
+
Dialog,
|
|
28
|
+
{
|
|
29
|
+
open,
|
|
30
|
+
onClose: () => !loading && onCancel(),
|
|
31
|
+
title,
|
|
32
|
+
size: "sm",
|
|
33
|
+
closeLabel,
|
|
34
|
+
footer: /* @__PURE__ */ jsxs(Fragment, { children: [
|
|
35
|
+
/* @__PURE__ */ jsx(Button, { ref: confirmRef, variant: danger ? "danger" : "primary", loading, onClick: onConfirm, children: confirmLabel }),
|
|
36
|
+
secondaryAction && /* @__PURE__ */ jsx(Button, { variant: "secondary", disabled: loading, onClick: secondaryAction.onClick, children: secondaryAction.label }),
|
|
37
|
+
/* @__PURE__ */ jsx(Button, { ref: cancelRef, variant: "secondary", disabled: loading, onClick: onCancel, children: cancelLabel })
|
|
38
|
+
] }),
|
|
39
|
+
children: (message || detail) && /* @__PURE__ */ jsxs("div", { className: "flex items-start gap-3", children: [
|
|
40
|
+
danger && /* @__PURE__ */ jsx("span", { className: "flex h-9 w-9 flex-none items-center justify-center rounded-full bg-incumprido-bg text-incumprido", children: /* @__PURE__ */ jsx(WarningIcon, { size: 18 }) }),
|
|
41
|
+
/* @__PURE__ */ jsxs("div", { className: "min-w-0 flex-1 self-center", children: [
|
|
42
|
+
message && /* @__PURE__ */ jsx("div", { className: "m-0 text-control leading-relaxed text-foreground", children: message }),
|
|
43
|
+
detail && /* @__PURE__ */ jsx("div", { className: "m-0 mt-1 text-caption leading-relaxed text-muted-foreground", children: detail })
|
|
44
|
+
] })
|
|
45
|
+
] })
|
|
46
|
+
}
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
const ConfirmContext = createContext(null);
|
|
50
|
+
function ConfirmProvider({ children, confirmLabel, cancelLabel, closeLabel }) {
|
|
51
|
+
const [pending, setPending] = useState(null);
|
|
52
|
+
const resolver = useRef(null);
|
|
53
|
+
const confirm = useCallback((options) => {
|
|
54
|
+
resolver.current?.(false);
|
|
55
|
+
setPending(options);
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
resolver.current = resolve;
|
|
58
|
+
});
|
|
59
|
+
}, []);
|
|
60
|
+
const answer = (ok) => {
|
|
61
|
+
resolver.current?.(ok);
|
|
62
|
+
resolver.current = null;
|
|
63
|
+
setPending(null);
|
|
64
|
+
};
|
|
65
|
+
return /* @__PURE__ */ jsxs(ConfirmContext.Provider, { value: confirm, children: [
|
|
66
|
+
children,
|
|
67
|
+
/* @__PURE__ */ jsx(
|
|
68
|
+
ConfirmDialog,
|
|
69
|
+
{
|
|
70
|
+
open: pending !== null,
|
|
71
|
+
title: pending?.title ?? "",
|
|
72
|
+
message: pending?.message,
|
|
73
|
+
detail: pending?.detail,
|
|
74
|
+
tone: pending?.tone,
|
|
75
|
+
secondaryAction: pending?.secondaryAction,
|
|
76
|
+
confirmLabel: pending?.confirmLabel ?? confirmLabel,
|
|
77
|
+
cancelLabel: pending?.cancelLabel ?? cancelLabel,
|
|
78
|
+
closeLabel: pending?.closeLabel ?? closeLabel,
|
|
79
|
+
onConfirm: () => answer(true),
|
|
80
|
+
onCancel: () => answer(false)
|
|
81
|
+
}
|
|
82
|
+
)
|
|
83
|
+
] });
|
|
84
|
+
}
|
|
85
|
+
function useConfirm() {
|
|
86
|
+
const confirm = useContext(ConfirmContext);
|
|
87
|
+
if (!confirm) throw new Error("useConfirm: falta o <ConfirmProvider> à volta da app");
|
|
88
|
+
return confirm;
|
|
89
|
+
}
|
|
90
|
+
export {
|
|
91
|
+
ConfirmProvider,
|
|
92
|
+
ConfirmDialog as default,
|
|
93
|
+
useConfirm
|
|
94
|
+
};
|
|
95
|
+
//# sourceMappingURL=ConfirmDialog.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ConfirmDialog.js","sources":["../../../../../../src/v1/components/overlays/ConfirmDialog/ConfirmDialog.tsx"],"sourcesContent":["import React, { createContext, useCallback, useContext, useEffect, useRef, useState } from 'react'\r\nimport Dialog from '../Dialog/Dialog'\r\nimport Button from '../../../primitives/buttons/Button/Button'\r\nimport { WarningIcon } from '../../../foundations/icons/icons'\r\n\r\nexport interface ConfirmDialogProps {\r\n open: boolean\r\n /** Curto: a ação ou a pergunta (ex: \"Apagar despesa\"). */\r\n title: React.ReactNode\r\n /** A pergunta ou o que vai acontecer (ex: \"Tem a certeza que pretende apagar esta despesa?\"). */\r\n message?: React.ReactNode\r\n /** Consequência que a pessoa não adivinha sozinha (ex: \"Não se pode desfazer.\"). Em letra mais pequena. */\r\n detail?: React.ReactNode\r\n /** `danger`: apaga ou não se pode desfazer. O botão fica vermelho e aparece o sinal de atenção. */\r\n tone?: 'default' | 'danger'\r\n /** Por omissão: \"Confirmar\". */\r\n confirmLabel?: string\r\n /** Por omissão: \"Cancelar\". */\r\n cancelLabel?: string\r\n /** Nome do botão ×, para leitores de ecrã. Por omissão: \"Fechar\". */\r\n closeLabel?: string\r\n /** A confirmar (ex: à espera do servidor): o botão mostra a roda e a janela não fecha. */\r\n loading?: boolean\r\n /** Uma segunda saída (ex: \"Desativar\" em vez de apagar). Não fecha a janela sozinha. */\r\n secondaryAction?: { label: string; onClick: () => void }\r\n onConfirm: () => void\r\n /** Cancelar, ×, Esc ou clique fora. */\r\n onCancel: () => void\r\n}\r\n\r\n/**\r\n * Pergunta de sim/não antes de uma ação (ex: apagar, enviar, fechar sem\r\n * guardar). Substitui o `window.confirm` do browser, que mostra o endereço do\r\n * servidor, não diz que registo é e só tem \"OK\".\r\n *\r\n * Com `tone=\"danger\"` o foco começa em Cancelar: um Enter por engano não apaga\r\n * nada. Nos outros casos começa em Confirmar.\r\n *\r\n * Para não ter de guardar o estado em cada ecrã, usar `useConfirm()` (com o\r\n * `ConfirmProvider` à volta da app).\r\n */\r\nexport default function ConfirmDialog({\r\n open,\r\n title,\r\n message,\r\n detail,\r\n tone = 'default',\r\n confirmLabel = 'Confirmar',\r\n cancelLabel = 'Cancelar',\r\n closeLabel = 'Fechar',\r\n loading = false,\r\n secondaryAction,\r\n onConfirm,\r\n onCancel,\r\n}: ConfirmDialogProps) {\r\n const danger = tone === 'danger'\r\n const confirmRef = useRef<HTMLButtonElement>(null)\r\n const cancelRef = useRef<HTMLButtonElement>(null)\r\n\r\n // Depois de a janela abrir (o efeito do Dialog corre antes deste): o browser\r\n // poria o foco no ×, o primeiro botão da janela.\r\n useEffect(() => {\r\n if (open) (danger ? cancelRef : confirmRef).current?.focus()\r\n }, [open, danger])\r\n\r\n return (\r\n <Dialog\r\n open={open}\r\n onClose={() => !loading && onCancel()}\r\n title={title}\r\n size=\"sm\"\r\n closeLabel={closeLabel}\r\n footer={\r\n <>\r\n <Button ref={confirmRef} variant={danger ? 'danger' : 'primary'} loading={loading} onClick={onConfirm}>\r\n {confirmLabel}\r\n </Button>\r\n {secondaryAction && (\r\n <Button variant=\"secondary\" disabled={loading} onClick={secondaryAction.onClick}>\r\n {secondaryAction.label}\r\n </Button>\r\n )}\r\n <Button ref={cancelRef} variant=\"secondary\" disabled={loading} onClick={onCancel}>\r\n {cancelLabel}\r\n </Button>\r\n </>\r\n }\r\n >\r\n {/* Sem texto não há corpo: o sinal de atenção sozinho ficava a flutuar. */}\r\n {(message || detail) && (\r\n <div className=\"flex items-start gap-3\">\r\n {danger && (\r\n <span className=\"flex h-9 w-9 flex-none items-center justify-center rounded-full bg-incumprido-bg text-incumprido\">\r\n <WarningIcon size={18} />\r\n </span>\r\n )}\r\n <div className=\"min-w-0 flex-1 self-center\">\r\n {message && <div className=\"m-0 text-control leading-relaxed text-foreground\">{message}</div>}\r\n {detail && <div className=\"m-0 mt-1 text-caption leading-relaxed text-muted-foreground\">{detail}</div>}\r\n </div>\r\n </div>\r\n )}\r\n </Dialog>\r\n )\r\n}\r\n\r\nexport type ConfirmOptions = Omit<ConfirmDialogProps, 'open' | 'onConfirm' | 'onCancel' | 'loading'>\r\n\r\nconst ConfirmContext = createContext<((options: ConfirmOptions) => Promise<boolean>) | null>(null)\r\n\r\nexport interface ConfirmProviderProps {\r\n children: React.ReactNode\r\n /** Textos por omissão dos botões, para a app os traduzir. */\r\n confirmLabel?: string\r\n cancelLabel?: string\r\n closeLabel?: string\r\n}\r\n\r\n/**\r\n * Dá o `useConfirm()` a toda a app. Monta-se uma vez, à volta da app.\r\n */\r\nexport function ConfirmProvider({ children, confirmLabel, cancelLabel, closeLabel }: ConfirmProviderProps) {\r\n const [pending, setPending] = useState<ConfirmOptions | null>(null)\r\n const resolver = useRef<((ok: boolean) => void) | null>(null)\r\n\r\n const confirm = useCallback((options: ConfirmOptions) => {\r\n // Uma pergunta nova responde \"não\" à anterior que ainda estivesse aberta.\r\n resolver.current?.(false)\r\n setPending(options)\r\n return new Promise<boolean>((resolve) => {\r\n resolver.current = resolve\r\n })\r\n }, [])\r\n\r\n const answer = (ok: boolean) => {\r\n resolver.current?.(ok)\r\n resolver.current = null\r\n setPending(null)\r\n }\r\n\r\n return (\r\n <ConfirmContext.Provider value={confirm}>\r\n {children}\r\n <ConfirmDialog\r\n open={pending !== null}\r\n title={pending?.title ?? ''}\r\n message={pending?.message}\r\n detail={pending?.detail}\r\n tone={pending?.tone}\r\n secondaryAction={pending?.secondaryAction}\r\n confirmLabel={pending?.confirmLabel ?? confirmLabel}\r\n cancelLabel={pending?.cancelLabel ?? cancelLabel}\r\n closeLabel={pending?.closeLabel ?? closeLabel}\r\n onConfirm={() => answer(true)}\r\n onCancel={() => answer(false)}\r\n />\r\n </ConfirmContext.Provider>\r\n )\r\n}\r\n\r\n/**\r\n * Pergunta e espera pela resposta: `true` se confirmou, `false` se cancelou.\r\n *\r\n * const confirm = useConfirm()\r\n * if (await confirm({ title: 'Apagar despesa', message: 'Tem a certeza?', tone: 'danger', confirmLabel: 'Apagar' })) apagar()\r\n */\r\nexport function useConfirm() {\r\n const confirm = useContext(ConfirmContext)\r\n if (!confirm) throw new Error('useConfirm: falta o <ConfirmProvider> à volta da app')\r\n return confirm\r\n}\r\n"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"Dialog.js","sources":["../../../../../../src/v1/components/overlays/Dialog/Dialog.tsx"],"sourcesContent":["import React, { createContext, useContext, useEffect, useId, useRef, useState } from 'react'\r\nimport { createPortal } from 'react-dom'\r\n\r\nconst DialogContainerContext = createContext<HTMLElement | null>(null)\r\nconst DialogDepthContext = createContext(0)\r\n\r\n/**\r\n * O elemento da janela, para usar como destino de `createPortal` (ex: avisos,\r\n * confirmações, sobreposições) quando o conteúdo deve ficar junto dela. Um\r\n * portal para `document.body` também funciona, desde que fique por cima\r\n * (`z-index` acima de 200). Fora de um Dialog devolve `null`.\r\n *\r\n * `createPortal(conteudo, useDialogContainer() ?? document.body)`\r\n */\r\nexport const useDialogContainer = () => useContext(DialogContainerContext)\r\n\r\nexport interface DialogProps {\r\n /** Aberta ou fechada. A app guarda este estado. */\r\n open: boolean\r\n /** Pedido para fechar (×, Esc ou clique fora): a app põe `open` a `false`. */\r\n onClose: () => void\r\n title: string\r\n /** Linha por baixo do título (ex: nome · departamento). */\r\n subtitle?: string\r\n /** Botões no rodapé, sempre visíveis (ex: Confirmar, Fechar). */\r\n footer?: React.ReactNode\r\n /** Largura máxima: `sm` 480px, `md` 720px, `lg` 1000px. Nunca passa o ecrã. */\r\n size?: 'sm' | 'md' | 'lg'\r\n /**\r\n * Altura: `auto` (por omissão) cresce com o conteúdo; `full` ocupa sempre a\r\n * altura disponível (útil quando o conteúdo muda de tamanho e a janela não\r\n * deve saltar). Em ambos, se o conteúdo não couber, o corpo faz scroll.\r\n */\r\n height?: 'auto' | 'full'\r\n /** Fechar ao clicar fora da janela. Por omissão sim; num formulário longo pode valer a pena desligar. */\r\n closeOnBackdrop?: boolean\r\n /** Nome do botão ×, para leitores de ecrã. Por omissão: \"Fechar\". */\r\n closeLabel?: string\r\n /**\r\n * Corpo sem margens e sem scroll: o conteúdo ocupa-o todo e gere o seu próprio\r\n * scroll (ex: passos que deslizam, rodapé próprio de cada passo).\r\n */\r\n flush?: boolean\r\n children: React.ReactNode\r\n}\r\n\r\nconst WIDTHS = { sm: 'max-w-dialog-sm', md: 'max-w-dialog-md', lg: 'max-w-dialog-lg' }\r\n\r\ninterface Aberta {\r\n root: HTMLElement\r\n janela: HTMLElement | null\r\n depth: number\r\n}\r\n\r\nconst abertas: Aberta[] = []\r\nconst silenciados = new Set<Element>()\r\n\r\nconst doTopo = () => abertas[abertas.length - 1]\r\n\r\nfunction silenciar() {\r\n for (const el of silenciados) el.removeAttribute('inert')\r\n silenciados.clear()\r\n const topo = doTopo()\r\n if (!topo) return\r\n for (const el of Array.from(document.body.children)) {\r\n if (el === topo.root || el.hasAttribute('inert')) continue\r\n el.setAttribute('inert', '')\r\n silenciados.add(el)\r\n }\r\n}\r\n\r\n// Uma janela aberta dentro de outra fica sempre por cima dela, mesmo que abram juntas.\r\nfunction empilhar(aberta: Aberta) {\r\n const acima = abertas.findIndex((a) => a.depth > aberta.depth)\r\n if (acima === -1) abertas.push(aberta)\r\n else abertas.splice(acima, 0, aberta)\r\n}\r\n\r\n/**\r\n * Janela por cima da página: cabeçalho navy com título, corpo que faz scroll e\r\n * rodapé com os botões.\r\n *\r\n * É a janela modal da V1 (substitui o `Modal` legacy).\r\n *\r\n * Fundo escurecido, Esc para fechar e, ao fechar, o foco volta ao sítio onde\r\n * estava. Enquanto está aberta, o resto da página fica\r\n * [inert](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inert):\r\n * nem rato, nem teclado, nem leitores de ecrã lhe chegam. Os portais abertos\r\n * depois dela (seletores, confirmações) continuam a responder.\r\n *\r\n * O conteúdo fica montado mesmo com a janela fechada, para poder carregar antes\r\n * de ela abrir.\r\n *\r\n * O tamanho adapta-se ao ecrã só com CSS: nunca passa a largura nem a altura\r\n * da janela do browser (com uma margem), e acompanha-a quando é\r\n * redimensionada. O que não couber faz scroll no corpo; o cabeçalho e o\r\n * rodapé ficam sempre visíveis.\r\n */\r\nexport default function Dialog({\r\n open,\r\n onClose,\r\n title,\r\n subtitle,\r\n footer,\r\n size = 'md',\r\n height = 'auto',\r\n closeOnBackdrop = true,\r\n closeLabel = 'Fechar',\r\n flush = false,\r\n children,\r\n}: DialogProps) {\r\n const [root, setRoot] = useState<HTMLDivElement | null>(null)\r\n const janela = useRef<HTMLDivElement>(null)\r\n const fundo = useRef<HTMLDivElement>(null)\r\n const titleId = useId()\r\n const depth = useContext(DialogDepthContext)\r\n const fechar = useRef(onClose)\r\n fechar.current = onClose\r\n // Clique fora: só conta se começou e acabou no fundo (arrastar a partir de um\r\n // campo para fora não fecha a janela).\r\n const pressedBackdrop = useRef(false)\r\n\r\n useEffect(() => {\r\n if (!open || !root) return\r\n const anterior = document.activeElement as HTMLElement | null\r\n const aberta = { root, janela: janela.current, depth }\r\n empilhar(aberta)\r\n silenciar()\r\n const topo = doTopo()\r\n const dentro = topo.janela?.contains(document.activeElement)\r\n if (!dentro) topo.janela?.focus()\r\n\r\n const onKeyDown = (e: KeyboardEvent) => {\r\n if (e.key !== 'Escape' || e.defaultPrevented) return\r\n if (doTopo() !== aberta) return\r\n fechar.current()\r\n }\r\n document.addEventListener('keydown', onKeyDown)\r\n\r\n return () => {\r\n document.removeEventListener('keydown', onKeyDown)\r\n abertas.splice(abertas.indexOf(aberta), 1)\r\n silenciar()\r\n const voltar = anterior && !anterior.closest('[inert]')\r\n if (voltar) anterior.focus()\r\n else doTopo()?.janela?.focus()\r\n }\r\n }, [open, root, depth])\r\n\r\n // A página por trás não faz scroll enquanto a janela está aberta.\r\n useEffect(() => {\r\n if (!open) return\r\n const html = document.documentElement\r\n const before = html.style.overflow\r\n html.style.overflow = 'hidden'\r\n return () => {\r\n html.style.overflow = before\r\n }\r\n }, [open])\r\n\r\n const noFundo = (e: React.SyntheticEvent) => e.target === fundo.current || e.target === e.currentTarget\r\n\r\n return createPortal(\r\n <div\r\n ref={setRoot}\r\n hidden={!open}\r\n onPointerDown={(e) => {\r\n pressedBackdrop.current = noFundo(e)\r\n }}\r\n onClick={(e) => {\r\n if (closeOnBackdrop && pressedBackdrop.current && noFundo(e)) onClose()\r\n }}\r\n className={`fixed inset-0 z-[200] items-center justify-center p-dialog-margin ${open ? 'flex' : 'hidden'}`}\r\n >\r\n <div ref={fundo} className=\"absolute inset-0 bg-black/40 motion-safe:animate-backdrop-in\" />\r\n <div\r\n ref={janela}\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n aria-labelledby={titleId}\r\n tabIndex={-1}\r\n className={\r\n `relative flex max-h-full w-full ${WIDTHS[size]} flex-col overflow-hidden rounded-2xl bg-card text-foreground shadow-modal outline-none ` +\r\n `motion-safe:animate-dialog-in ${height === 'full' ? 'h-full' : ''}`\r\n }\r\n >\r\n <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\">\r\n <div className=\"min-w-0\">\r\n <h2 id={titleId} className=\"m-0 truncate text-title font-bold text-primary-foreground\">\r\n {title}\r\n </h2>\r\n {subtitle && <p className=\"m-0 mt-0.5 truncate text-caption text-primary-foreground/75\">{subtitle}</p>}\r\n </div>\r\n <button\r\n type=\"button\"\r\n aria-label={closeLabel}\r\n title={closeLabel}\r\n onClick={onClose}\r\n className=\"flex h-7 w-7 flex-none cursor-pointer items-center justify-center rounded-md border border-primary-foreground/25 bg-primary-foreground/15 text-base leading-none text-primary-foreground transition-colors hover:bg-primary-foreground/25 focus-visible:ring-2 focus-visible:ring-primary-foreground/60 focus-visible:outline-none\"\r\n >\r\n ×\r\n </button>\r\n </header>\r\n\r\n <DialogContainerContext.Provider value={root}>\r\n <DialogDepthContext.Provider value={depth + 1}>\r\n <div className={flush ? 'flex min-h-0 flex-1 flex-col overflow-hidden bg-canvas' : 'min-h-0 flex-1 overflow-y-auto bg-canvas px-5 py-4'}>{children}</div>\r\n\r\n {footer && <footer className=\"flex flex-none flex-wrap justify-end gap-2 border-t border-border bg-muted/40 px-5 py-[18px]\">{footer}</footer>}\r\n </DialogDepthContext.Provider>\r\n </DialogContainerContext.Provider>\r\n </div>\r\n </div>,\r\n document.body,\r\n )\r\n}\r\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"Dialog.js","sources":["../../../../../../src/v1/components/overlays/Dialog/Dialog.tsx"],"sourcesContent":["import React, { createContext, useContext, useEffect, useId, useRef, useState } from 'react'\r\nimport { createPortal } from 'react-dom'\r\n\r\nconst DialogContainerContext = createContext<HTMLElement | null>(null)\r\nconst DialogDepthContext = createContext(0)\r\n\r\n/**\r\n * O elemento da janela, para usar como destino de `createPortal` (ex: avisos,\r\n * confirmações, sobreposições) quando o conteúdo deve ficar junto dela. Um\r\n * portal para `document.body` também funciona, desde que fique por cima\r\n * (`z-index` acima de 200). Fora de um Dialog devolve `null`.\r\n *\r\n * `createPortal(conteudo, useDialogContainer() ?? document.body)`\r\n */\r\nexport const useDialogContainer = () => useContext(DialogContainerContext)\r\n\r\nexport interface DialogProps {\r\n /** Aberta ou fechada. A app guarda este estado. */\r\n open: boolean\r\n /** Pedido para fechar (×, Esc ou clique fora): a app põe `open` a `false`. */\r\n onClose: () => void\r\n /** Normalmente texto; aceita JSX (ex: um nome em negrito). */\r\n title: React.ReactNode\r\n /** Linha por baixo do título (ex: nome · departamento). */\r\n subtitle?: React.ReactNode\r\n /** Botões no rodapé, sempre visíveis (ex: Confirmar, Fechar). */\r\n footer?: React.ReactNode\r\n /** Largura máxima: `sm` 480px, `md` 720px, `lg` 1000px. Nunca passa o ecrã. */\r\n size?: 'sm' | 'md' | 'lg'\r\n /**\r\n * Altura: `auto` (por omissão) cresce com o conteúdo; `full` ocupa sempre a\r\n * altura disponível (útil quando o conteúdo muda de tamanho e a janela não\r\n * deve saltar). Em ambos, se o conteúdo não couber, o corpo faz scroll.\r\n */\r\n height?: 'auto' | 'full'\r\n /** Fechar ao clicar fora da janela. Por omissão sim; num formulário longo pode valer a pena desligar. */\r\n closeOnBackdrop?: boolean\r\n /** Nome do botão ×, para leitores de ecrã. Por omissão: \"Fechar\". */\r\n closeLabel?: string\r\n /**\r\n * Corpo sem margens e sem scroll: o conteúdo ocupa-o todo e gere o seu próprio\r\n * scroll (ex: passos que deslizam, rodapé próprio de cada passo).\r\n */\r\n flush?: boolean\r\n children: React.ReactNode\r\n}\r\n\r\nconst WIDTHS = { sm: 'max-w-dialog-sm', md: 'max-w-dialog-md', lg: 'max-w-dialog-lg' }\r\n\r\ninterface Aberta {\r\n root: HTMLElement\r\n janela: HTMLElement | null\r\n depth: number\r\n}\r\n\r\nconst abertas: Aberta[] = []\r\nconst silenciados = new Set<Element>()\r\n\r\nconst doTopo = () => abertas[abertas.length - 1]\r\n\r\nfunction silenciar() {\r\n for (const el of silenciados) el.removeAttribute('inert')\r\n silenciados.clear()\r\n const topo = doTopo()\r\n if (!topo) return\r\n for (const el of Array.from(document.body.children)) {\r\n if (el === topo.root || el.hasAttribute('inert')) continue\r\n el.setAttribute('inert', '')\r\n silenciados.add(el)\r\n }\r\n}\r\n\r\n// Uma janela aberta dentro de outra fica sempre por cima dela, mesmo que abram juntas.\r\nfunction empilhar(aberta: Aberta) {\r\n const acima = abertas.findIndex((a) => a.depth > aberta.depth)\r\n if (acima === -1) abertas.push(aberta)\r\n else abertas.splice(acima, 0, aberta)\r\n}\r\n\r\n/**\r\n * Janela por cima da página: cabeçalho navy com título, corpo que faz scroll e\r\n * rodapé com os botões ([Button](?path=/docs/v1-primitives-buttons-button--docs)).\r\n *\r\n * É a janela modal da V1 (substitui o `Modal` legacy).\r\n *\r\n * Fundo escurecido, Esc para fechar e, ao fechar, o foco volta ao sítio onde\r\n * estava. Enquanto está aberta, o resto da página fica\r\n * [inert](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Global_attributes/inert):\r\n * nem rato, nem teclado, nem leitores de ecrã lhe chegam. Os portais abertos\r\n * depois dela (seletores, confirmações) continuam a responder.\r\n *\r\n * O conteúdo fica montado mesmo com a janela fechada, para poder carregar antes\r\n * de ela abrir.\r\n *\r\n * O tamanho adapta-se ao ecrã só com CSS: nunca passa a largura nem a altura\r\n * da janela do browser (com uma margem), e acompanha-a quando é\r\n * redimensionada. O que não couber faz scroll no corpo; o cabeçalho e o\r\n * rodapé ficam sempre visíveis.\r\n *\r\n * **Exemplo completo:** [Formulário em modal](?path=/docs/v1-patterns-forms-formulário-em-modal--docs).\r\n */\r\nexport default function Dialog({\r\n open,\r\n onClose,\r\n title,\r\n subtitle,\r\n footer,\r\n size = 'md',\r\n height = 'auto',\r\n closeOnBackdrop = true,\r\n closeLabel = 'Fechar',\r\n flush = false,\r\n children,\r\n}: DialogProps) {\r\n const [root, setRoot] = useState<HTMLDivElement | null>(null)\r\n const janela = useRef<HTMLDivElement>(null)\r\n const fundo = useRef<HTMLDivElement>(null)\r\n const titleId = useId()\r\n const depth = useContext(DialogDepthContext)\r\n const fechar = useRef(onClose)\r\n fechar.current = onClose\r\n // Clique fora: só conta se começou e acabou no fundo (arrastar a partir de um\r\n // campo para fora não fecha a janela).\r\n const pressedBackdrop = useRef(false)\r\n\r\n useEffect(() => {\r\n if (!open || !root) return\r\n const anterior = document.activeElement as HTMLElement | null\r\n const aberta = { root, janela: janela.current, depth }\r\n empilhar(aberta)\r\n silenciar()\r\n const topo = doTopo()\r\n const dentro = topo.janela?.contains(document.activeElement)\r\n if (!dentro) topo.janela?.focus()\r\n\r\n const onKeyDown = (e: KeyboardEvent) => {\r\n if (e.key !== 'Escape' || e.defaultPrevented) return\r\n if (doTopo() !== aberta) return\r\n fechar.current()\r\n }\r\n document.addEventListener('keydown', onKeyDown)\r\n\r\n return () => {\r\n document.removeEventListener('keydown', onKeyDown)\r\n abertas.splice(abertas.indexOf(aberta), 1)\r\n silenciar()\r\n const voltar = anterior && !anterior.closest('[inert]')\r\n if (voltar) anterior.focus()\r\n else doTopo()?.janela?.focus()\r\n }\r\n }, [open, root, depth])\r\n\r\n // A página por trás não faz scroll enquanto a janela está aberta.\r\n useEffect(() => {\r\n if (!open) return\r\n const html = document.documentElement\r\n const before = html.style.overflow\r\n html.style.overflow = 'hidden'\r\n return () => {\r\n html.style.overflow = before\r\n }\r\n }, [open])\r\n\r\n const noFundo = (e: React.SyntheticEvent) => e.target === fundo.current || e.target === e.currentTarget\r\n\r\n return createPortal(\r\n <div\r\n ref={setRoot}\r\n hidden={!open}\r\n onPointerDown={(e) => {\r\n pressedBackdrop.current = noFundo(e)\r\n }}\r\n onClick={(e) => {\r\n if (closeOnBackdrop && pressedBackdrop.current && noFundo(e)) onClose()\r\n }}\r\n className={`fixed inset-0 z-[200] items-center justify-center p-dialog-margin ${open ? 'flex' : 'hidden'}`}\r\n >\r\n <div ref={fundo} className=\"absolute inset-0 bg-black/40 motion-safe:animate-backdrop-in\" />\r\n <div\r\n ref={janela}\r\n role=\"dialog\"\r\n aria-modal=\"true\"\r\n aria-labelledby={titleId}\r\n tabIndex={-1}\r\n className={\r\n `relative flex max-h-full w-full ${WIDTHS[size]} flex-col overflow-hidden rounded-2xl bg-card text-foreground shadow-modal outline-none ` +\r\n `motion-safe:animate-dialog-in ${height === 'full' ? 'h-full' : ''}`\r\n }\r\n >\r\n <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\">\r\n <div className=\"min-w-0\">\r\n <h2 id={titleId} className=\"m-0 truncate text-title font-bold text-primary-foreground\">\r\n {title}\r\n </h2>\r\n {subtitle && <p className=\"m-0 mt-0.5 truncate text-caption text-primary-foreground/75\">{subtitle}</p>}\r\n </div>\r\n <button\r\n type=\"button\"\r\n aria-label={closeLabel}\r\n title={closeLabel}\r\n onClick={onClose}\r\n className=\"flex h-7 w-7 flex-none cursor-pointer items-center justify-center rounded-md border border-primary-foreground/25 bg-primary-foreground/15 text-base leading-none text-primary-foreground transition-colors hover:bg-primary-foreground/25 focus-visible:ring-2 focus-visible:ring-primary-foreground/60 focus-visible:outline-none\"\r\n >\r\n ×\r\n </button>\r\n </header>\r\n\r\n <DialogContainerContext.Provider value={root}>\r\n <DialogDepthContext.Provider value={depth + 1}>\r\n <div className={flush ? 'flex min-h-0 flex-1 flex-col overflow-hidden bg-canvas' : 'min-h-0 flex-1 overflow-y-auto bg-canvas px-5 py-4'}>{children}</div>\r\n\r\n {footer && <footer className=\"flex flex-none flex-wrap justify-end gap-2 border-t border-border bg-muted/40 px-5 py-[18px]\">{footer}</footer>}\r\n </DialogDepthContext.Provider>\r\n </DialogContainerContext.Provider>\r\n </div>\r\n </div>,\r\n document.body,\r\n )\r\n}\r\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useId, useEffect, useLayoutEffect } from "react";
|
|
3
|
+
import { useTopLayer } from "../topLayer.js";
|
|
4
|
+
import TooltipPanel, { MARGIN, OFFSET, clamp } from "./TooltipPanel.js";
|
|
5
|
+
const SHOW_DELAY = 120;
|
|
6
|
+
const HIDE_DELAY_INTERACTIVE = 200;
|
|
7
|
+
const CURSOR_OFFSET = 14;
|
|
8
|
+
function Tooltip({
|
|
9
|
+
content: contentProp,
|
|
10
|
+
tip,
|
|
11
|
+
title,
|
|
12
|
+
children,
|
|
13
|
+
interactive = false,
|
|
14
|
+
followCursor = false,
|
|
15
|
+
disabled = false,
|
|
16
|
+
className = ""
|
|
17
|
+
}) {
|
|
18
|
+
const content = contentProp ?? tip;
|
|
19
|
+
const [open, setOpen] = useState(false);
|
|
20
|
+
const [cursor, setCursor] = useState(null);
|
|
21
|
+
const anchorRef = useRef(null);
|
|
22
|
+
const panelRef = useRef(null);
|
|
23
|
+
const timer = useRef(null);
|
|
24
|
+
const id = useId();
|
|
25
|
+
useTopLayer(panelRef, open);
|
|
26
|
+
const clear = () => {
|
|
27
|
+
if (timer.current) clearTimeout(timer.current);
|
|
28
|
+
timer.current = null;
|
|
29
|
+
};
|
|
30
|
+
const show = () => {
|
|
31
|
+
clear();
|
|
32
|
+
timer.current = setTimeout(() => setOpen(true), SHOW_DELAY);
|
|
33
|
+
};
|
|
34
|
+
const hide = () => {
|
|
35
|
+
clear();
|
|
36
|
+
if (interactive) timer.current = setTimeout(() => setOpen(false), HIDE_DELAY_INTERACTIVE);
|
|
37
|
+
else setOpen(false);
|
|
38
|
+
};
|
|
39
|
+
useEffect(() => clear, []);
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
if (!open) return;
|
|
42
|
+
const onKey = (e) => {
|
|
43
|
+
if (e.key === "Escape") setOpen(false);
|
|
44
|
+
};
|
|
45
|
+
document.addEventListener("keydown", onKey);
|
|
46
|
+
return () => document.removeEventListener("keydown", onKey);
|
|
47
|
+
}, [open]);
|
|
48
|
+
const [pos, setPos] = useState({ left: 0, top: 0 });
|
|
49
|
+
useLayoutEffect(() => {
|
|
50
|
+
if (!open || !anchorRef.current || !panelRef.current) return;
|
|
51
|
+
const p = panelRef.current.getBoundingClientRect();
|
|
52
|
+
if (followCursor && cursor) {
|
|
53
|
+
const left = cursor.x + CURSOR_OFFSET + p.width > window.innerWidth - MARGIN ? Math.max(MARGIN, cursor.x - CURSOR_OFFSET - p.width) : cursor.x + CURSOR_OFFSET;
|
|
54
|
+
const top2 = cursor.y + CURSOR_OFFSET + p.height > window.innerHeight - MARGIN ? Math.max(MARGIN, cursor.y - CURSOR_OFFSET - p.height) : cursor.y + CURSOR_OFFSET;
|
|
55
|
+
setPos({ left, top: top2 });
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
const a = anchorRef.current.getBoundingClientRect();
|
|
59
|
+
const above = a.top - OFFSET - p.height;
|
|
60
|
+
const top = above >= 0 ? above : a.bottom + OFFSET;
|
|
61
|
+
setPos({ left: clamp(a.left + a.width / 2 - p.width / 2, p.width, window.innerWidth), top });
|
|
62
|
+
}, [open, followCursor, cursor]);
|
|
63
|
+
if (disabled || !content) return /* @__PURE__ */ jsx("span", { className: className || void 0, children });
|
|
64
|
+
return /* @__PURE__ */ jsxs(
|
|
65
|
+
"span",
|
|
66
|
+
{
|
|
67
|
+
ref: anchorRef,
|
|
68
|
+
onMouseEnter: show,
|
|
69
|
+
onMouseMove: followCursor ? (e) => setCursor({ x: e.clientX, y: e.clientY }) : void 0,
|
|
70
|
+
onMouseLeave: hide,
|
|
71
|
+
onFocus: show,
|
|
72
|
+
onBlur: hide,
|
|
73
|
+
"aria-describedby": open ? id : void 0,
|
|
74
|
+
className: className || void 0,
|
|
75
|
+
children: [
|
|
76
|
+
children,
|
|
77
|
+
/* @__PURE__ */ jsx(
|
|
78
|
+
TooltipPanel,
|
|
79
|
+
{
|
|
80
|
+
ref: panelRef,
|
|
81
|
+
id,
|
|
82
|
+
visible: open,
|
|
83
|
+
title,
|
|
84
|
+
interactive,
|
|
85
|
+
style: { left: pos.left, top: pos.top },
|
|
86
|
+
onMouseEnter: interactive ? clear : void 0,
|
|
87
|
+
onMouseLeave: interactive ? hide : void 0,
|
|
88
|
+
children: /* @__PURE__ */ jsx("div", { className: "whitespace-pre-line", children: content })
|
|
89
|
+
}
|
|
90
|
+
)
|
|
91
|
+
]
|
|
92
|
+
}
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
Tooltip as default
|
|
97
|
+
};
|
|
98
|
+
//# sourceMappingURL=Tooltip.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"Tooltip.js","sources":["../../../../../../src/v1/components/overlays/Tooltip/Tooltip.tsx"],"sourcesContent":["import React, { useEffect, useId, useLayoutEffect, useRef, useState } from 'react'\r\nimport { useTopLayer } from '../topLayer'\r\nimport TooltipPanel, { MARGIN, OFFSET, clamp } from './TooltipPanel'\r\n\r\nexport interface TooltipProps {\r\n /** O que aparece no painel. Aceita JSX (ex: uma lista, um link com `interactive`). Vazio: sem tooltip. */\r\n content?: React.ReactNode\r\n /** @deprecated Nome antigo de `content` (Tooltip legacy). */\r\n tip?: React.ReactNode\r\n /** @deprecated Já não é preciso (o id é gerado); fica para não partir o código legacy. */\r\n id?: string\r\n /** Cabeçalho opcional, separado por uma linha. */\r\n title?: React.ReactNode\r\n /** O elemento que mostra o tooltip ao passar o rato ou com o foco. */\r\n children: React.ReactNode\r\n /** O painel aceita o rato (para clicar em links lá dentro) e fecha com um pequeno atraso. */\r\n interactive?: boolean\r\n /**\r\n * O painel segue o rato, em vez de ficar por cima do elemento. Para elementos\r\n * grandes ou células de grelhas (ex: um cronograma), onde o centro do\r\n * elemento fica longe do rato.\r\n */\r\n followCursor?: boolean\r\n /** Desliga o tooltip (o elemento aparece na mesma). */\r\n disabled?: boolean\r\n /** Classes do invólucro (um `<span>`), ex: `cursor-help` ou `block`. */\r\n className?: string\r\n}\r\n\r\nconst SHOW_DELAY = 120\r\nconst HIDE_DELAY_INTERACTIVE = 200\r\n/** Distância do cursor ao painel, com `followCursor`. */\r\nconst CURSOR_OFFSET = 14\r\n\r\n/**\r\n * Tooltip com conteúdo rico à volta de um elemento: aparece por cima dele (ou\r\n * por baixo, se não couber) ao passar o rato ou com o foco do teclado; Esc\r\n * fecha. As quebras de linha (`\\n`) do texto mantêm-se.\r\n *\r\n * Para texto simples, basta o atributo `data-tip` com o `TooltipLayer`\r\n * montado na raiz, sem embrulhar nada. O painel é o mesmo.\r\n */\r\nexport default function Tooltip({\r\n content: contentProp,\r\n tip,\r\n title,\r\n children,\r\n interactive = false,\r\n followCursor = false,\r\n disabled = false,\r\n className = '',\r\n}: TooltipProps) {\r\n const content = contentProp ?? tip\r\n const [open, setOpen] = useState(false)\r\n const [cursor, setCursor] = useState<{ x: number; y: number } | null>(null)\r\n const anchorRef = useRef<HTMLSpanElement>(null)\r\n const panelRef = useRef<HTMLDivElement>(null)\r\n const timer = useRef<ReturnType<typeof setTimeout> | null>(null)\r\n const id = useId()\r\n\r\n useTopLayer(panelRef, open)\r\n\r\n const clear = () => {\r\n if (timer.current) clearTimeout(timer.current)\r\n timer.current = null\r\n }\r\n const show = () => {\r\n clear()\r\n timer.current = setTimeout(() => setOpen(true), SHOW_DELAY)\r\n }\r\n const hide = () => {\r\n clear()\r\n if (interactive) timer.current = setTimeout(() => setOpen(false), HIDE_DELAY_INTERACTIVE)\r\n else setOpen(false)\r\n }\r\n\r\n useEffect(() => clear, [])\r\n\r\n useEffect(() => {\r\n if (!open) return\r\n const onKey = (e: KeyboardEvent) => {\r\n if (e.key === 'Escape') setOpen(false)\r\n }\r\n document.addEventListener('keydown', onKey)\r\n return () => document.removeEventListener('keydown', onKey)\r\n }, [open])\r\n\r\n // Por cima do elemento, centrado (por baixo se não couber em cima); ou, com\r\n // `followCursor`, à direita e abaixo do rato, virando quando não cabe.\r\n const [pos, setPos] = useState({ left: 0, top: 0 })\r\n useLayoutEffect(() => {\r\n if (!open || !anchorRef.current || !panelRef.current) return\r\n const p = panelRef.current.getBoundingClientRect()\r\n if (followCursor && cursor) {\r\n const left = cursor.x + CURSOR_OFFSET + p.width > window.innerWidth - MARGIN ? Math.max(MARGIN, cursor.x - CURSOR_OFFSET - p.width) : cursor.x + CURSOR_OFFSET\r\n const top = cursor.y + CURSOR_OFFSET + p.height > window.innerHeight - MARGIN ? Math.max(MARGIN, cursor.y - CURSOR_OFFSET - p.height) : cursor.y + CURSOR_OFFSET\r\n setPos({ left, top })\r\n return\r\n }\r\n const a = anchorRef.current.getBoundingClientRect()\r\n const above = a.top - OFFSET - p.height\r\n const top = above >= 0 ? above : a.bottom + OFFSET\r\n setPos({ left: clamp(a.left + a.width / 2 - p.width / 2, p.width, window.innerWidth), top })\r\n }, [open, followCursor, cursor])\r\n\r\n if (disabled || !content) return <span className={className || undefined}>{children}</span>\r\n\r\n return (\r\n <span\r\n ref={anchorRef}\r\n onMouseEnter={show}\r\n onMouseMove={followCursor ? (e) => setCursor({ x: e.clientX, y: e.clientY }) : undefined}\r\n onMouseLeave={hide}\r\n onFocus={show}\r\n onBlur={hide}\r\n aria-describedby={open ? id : undefined}\r\n className={className || undefined}\r\n >\r\n {children}\r\n <TooltipPanel\r\n ref={panelRef}\r\n id={id}\r\n visible={open}\r\n title={title}\r\n interactive={interactive}\r\n style={{ left: pos.left, top: pos.top }}\r\n onMouseEnter={interactive ? clear : undefined}\r\n onMouseLeave={interactive ? hide : undefined}\r\n >\r\n <div className=\"whitespace-pre-line\">{content}</div>\r\n </TooltipPanel>\r\n </span>\r\n )\r\n}\r\n"],"names":["top"],"mappings":"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"}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { jsx } from "react/jsx-runtime";
|
|
2
|
+
import { useState, useRef, useEffect, useLayoutEffect } from "react";
|
|
3
|
+
import { useTopLayer } from "../topLayer.js";
|
|
4
|
+
import TooltipPanel, { MARGIN } from "./TooltipPanel.js";
|
|
5
|
+
const CURSOR_OFFSET = 14;
|
|
6
|
+
const DELAY_MS = 120;
|
|
7
|
+
const tipOf = (el) => {
|
|
8
|
+
const target = el?.closest("[data-tip]") ?? null;
|
|
9
|
+
const text = target?.getAttribute("data-tip");
|
|
10
|
+
return target && text ? { target, text, title: target.getAttribute("data-tip-title") } : null;
|
|
11
|
+
};
|
|
12
|
+
function TooltipLayer() {
|
|
13
|
+
const [tip, setTip] = useState(null);
|
|
14
|
+
const panelRef = useRef(null);
|
|
15
|
+
const sourceRef = useRef(null);
|
|
16
|
+
const timerRef = useRef(null);
|
|
17
|
+
useTopLayer(panelRef, tip !== null);
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
const clearTimer = () => {
|
|
20
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
21
|
+
timerRef.current = null;
|
|
22
|
+
};
|
|
23
|
+
const hide = () => {
|
|
24
|
+
clearTimer();
|
|
25
|
+
sourceRef.current = null;
|
|
26
|
+
setTip((prev) => prev ? null : prev);
|
|
27
|
+
};
|
|
28
|
+
const show = (target, next) => {
|
|
29
|
+
if (sourceRef.current === target) {
|
|
30
|
+
setTip(next);
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
clearTimer();
|
|
34
|
+
sourceRef.current = target;
|
|
35
|
+
timerRef.current = setTimeout(() => setTip(next), DELAY_MS);
|
|
36
|
+
};
|
|
37
|
+
const onMove = (e) => {
|
|
38
|
+
const found = tipOf(e.target instanceof Element ? e.target : null);
|
|
39
|
+
if (!found) return hide();
|
|
40
|
+
show(found.target, { text: found.text, title: found.title, x: e.clientX, y: e.clientY });
|
|
41
|
+
};
|
|
42
|
+
const onFocus = (e) => {
|
|
43
|
+
const el = e.target instanceof Element ? e.target : null;
|
|
44
|
+
if (!el?.matches(":focus-visible")) return;
|
|
45
|
+
const found = tipOf(el);
|
|
46
|
+
if (!found) return hide();
|
|
47
|
+
const r = found.target.getBoundingClientRect();
|
|
48
|
+
show(found.target, { text: found.text, title: found.title, x: r.left, y: r.bottom - CURSOR_OFFSET + 6 });
|
|
49
|
+
};
|
|
50
|
+
const onKey = (e) => {
|
|
51
|
+
if (e.key === "Escape") hide();
|
|
52
|
+
};
|
|
53
|
+
document.addEventListener("mousemove", onMove, { passive: true });
|
|
54
|
+
document.addEventListener("focusin", onFocus);
|
|
55
|
+
document.addEventListener("focusout", hide);
|
|
56
|
+
document.addEventListener("keydown", onKey);
|
|
57
|
+
document.addEventListener("mousedown", hide, true);
|
|
58
|
+
document.addEventListener("wheel", hide, { passive: true, capture: true });
|
|
59
|
+
window.addEventListener("blur", hide);
|
|
60
|
+
return () => {
|
|
61
|
+
clearTimer();
|
|
62
|
+
document.removeEventListener("mousemove", onMove);
|
|
63
|
+
document.removeEventListener("focusin", onFocus);
|
|
64
|
+
document.removeEventListener("focusout", hide);
|
|
65
|
+
document.removeEventListener("keydown", onKey);
|
|
66
|
+
document.removeEventListener("mousedown", hide, true);
|
|
67
|
+
document.removeEventListener("wheel", hide, true);
|
|
68
|
+
window.removeEventListener("blur", hide);
|
|
69
|
+
};
|
|
70
|
+
}, []);
|
|
71
|
+
const [pos, setPos] = useState({ left: 0, top: 0 });
|
|
72
|
+
useLayoutEffect(() => {
|
|
73
|
+
if (!tip || !panelRef.current) return;
|
|
74
|
+
const r = panelRef.current.getBoundingClientRect();
|
|
75
|
+
const left = tip.x + CURSOR_OFFSET + r.width > window.innerWidth - MARGIN ? Math.max(MARGIN, tip.x - CURSOR_OFFSET - r.width) : tip.x + CURSOR_OFFSET;
|
|
76
|
+
const top = tip.y + CURSOR_OFFSET + r.height > window.innerHeight - MARGIN ? Math.max(MARGIN, tip.y - CURSOR_OFFSET - r.height) : tip.y + CURSOR_OFFSET;
|
|
77
|
+
setPos({ left, top });
|
|
78
|
+
}, [tip]);
|
|
79
|
+
return /* @__PURE__ */ jsx(TooltipPanel, { ref: panelRef, visible: tip !== null, title: tip?.title, style: { left: pos.left, top: pos.top }, children: /* @__PURE__ */ jsx("p", { className: "m-0 whitespace-pre-line", children: tip?.text }) });
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
TooltipLayer as default
|
|
83
|
+
};
|
|
84
|
+
//# sourceMappingURL=TooltipLayer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TooltipLayer.js","sources":["../../../../../../src/v1/components/overlays/Tooltip/TooltipLayer.tsx"],"sourcesContent":["import React, { useEffect, useLayoutEffect, useRef, useState } from 'react'\r\nimport { useTopLayer } from '../topLayer'\r\nimport TooltipPanel, { MARGIN } from './TooltipPanel'\r\n\r\n/** Distância do cursor ao painel. */\r\nconst CURSOR_OFFSET = 14\r\n/** Atraso curto: evita painéis a piscar ao atravessar uma barra de botões. */\r\nconst DELAY_MS = 120\r\n\r\ninterface Tip {\r\n text: string\r\n title: string | null\r\n x: number\r\n y: number\r\n}\r\n\r\nconst tipOf = (el: Element | null) => {\r\n const target = el?.closest('[data-tip]') ?? null\r\n const text = target?.getAttribute('data-tip')\r\n // `data-tip` vazio é o mesmo que não ter (o React omite o atributo quando o valor é undefined).\r\n return target && text ? { target, text, title: target.getAttribute('data-tip-title') } : null\r\n}\r\n\r\n/**\r\n * Tooltips de toda a app, por atributo: qualquer elemento com `data-tip` mostra\r\n * o texto num painel escuro ao passar o rato ou ao receber o foco do teclado.\r\n * Monta-se uma vez na raiz.\r\n *\r\n * <button data-tip=\"Registar horas\" aria-label=\"Registar horas\">…</button>\r\n * <span data-tip={nome} data-tip-title=\"Requisitante\">…</span>\r\n *\r\n * - `data-tip-title`: cabeçalho opcional, separado por uma linha.\r\n * - Texto com `\\n` aparece em várias linhas.\r\n * - Para conteúdo rico (JSX, links), usar o `Tooltip`.\r\n * - Substitui o `title` do browser (lento, sem tema, corta texto longo). O\r\n * tooltip não é lido pelos leitores de ecrã: o elemento continua a precisar\r\n * do seu nome (`aria-label` ou texto).\r\n * - Aparece por cima de uma janela (Dialog) aberta.\r\n */\r\nexport default function TooltipLayer() {\r\n const [tip, setTip] = useState<Tip | null>(null)\r\n const panelRef = useRef<HTMLDivElement>(null)\r\n /** Elemento que originou o tooltip visível, para não recalcular a cada pixel. */\r\n const sourceRef = useRef<Element | null>(null)\r\n const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)\r\n\r\n useTopLayer(panelRef, tip !== null)\r\n\r\n useEffect(() => {\r\n const clearTimer = () => {\r\n if (timerRef.current) clearTimeout(timerRef.current)\r\n timerRef.current = null\r\n }\r\n const hide = () => {\r\n clearTimer()\r\n sourceRef.current = null\r\n setTip((prev) => (prev ? null : prev))\r\n }\r\n const show = (target: Element, next: Tip) => {\r\n // Já visível sobre o mesmo elemento: só segue o cursor, sem atraso.\r\n if (sourceRef.current === target) {\r\n setTip(next)\r\n return\r\n }\r\n clearTimer()\r\n sourceRef.current = target\r\n timerRef.current = setTimeout(() => setTip(next), DELAY_MS)\r\n }\r\n\r\n const onMove = (e: MouseEvent) => {\r\n const found = tipOf(e.target instanceof Element ? e.target : null)\r\n if (!found) return hide()\r\n show(found.target, { text: found.text, title: found.title, x: e.clientX, y: e.clientY })\r\n }\r\n\r\n // Teclado: o painel aparece por baixo do elemento focado.\r\n const onFocus = (e: FocusEvent) => {\r\n const el = e.target instanceof Element ? e.target : null\r\n if (!el?.matches(':focus-visible')) return\r\n const found = tipOf(el)\r\n if (!found) return hide()\r\n const r = found.target.getBoundingClientRect()\r\n show(found.target, { text: found.text, title: found.title, x: r.left, y: r.bottom - CURSOR_OFFSET + 6 })\r\n }\r\n\r\n const onKey = (e: KeyboardEvent) => {\r\n if (e.key === 'Escape') hide()\r\n }\r\n\r\n // Fechar em qualquer interação: um painel a pairar sobre um menu que acabou\r\n // de abrir é pior do que não haver tooltip.\r\n document.addEventListener('mousemove', onMove, { passive: true })\r\n document.addEventListener('focusin', onFocus)\r\n document.addEventListener('focusout', hide)\r\n document.addEventListener('keydown', onKey)\r\n document.addEventListener('mousedown', hide, true)\r\n document.addEventListener('wheel', hide, { passive: true, capture: true })\r\n window.addEventListener('blur', hide)\r\n return () => {\r\n clearTimer()\r\n document.removeEventListener('mousemove', onMove)\r\n document.removeEventListener('focusin', onFocus)\r\n document.removeEventListener('focusout', hide)\r\n document.removeEventListener('keydown', onKey)\r\n document.removeEventListener('mousedown', hide, true)\r\n document.removeEventListener('wheel', hide, true)\r\n window.removeEventListener('blur', hide)\r\n }\r\n }, [])\r\n\r\n // À direita e abaixo do cursor, virando para o lado oposto quando não cabe.\r\n // Medido antes de pintar, com as dimensões reais do painel.\r\n const [pos, setPos] = useState({ left: 0, top: 0 })\r\n useLayoutEffect(() => {\r\n if (!tip || !panelRef.current) return\r\n const r = panelRef.current.getBoundingClientRect()\r\n const left =\r\n tip.x + CURSOR_OFFSET + r.width > window.innerWidth - MARGIN ? Math.max(MARGIN, tip.x - CURSOR_OFFSET - r.width) : tip.x + CURSOR_OFFSET\r\n const top =\r\n tip.y + CURSOR_OFFSET + r.height > window.innerHeight - MARGIN ? Math.max(MARGIN, tip.y - CURSOR_OFFSET - r.height) : tip.y + CURSOR_OFFSET\r\n setPos({ left, top })\r\n }, [tip])\r\n\r\n return (\r\n <TooltipPanel ref={panelRef} visible={tip !== null} title={tip?.title} style={{ left: pos.left, top: pos.top }}>\r\n <p className=\"m-0 whitespace-pre-line\">{tip?.text}</p>\r\n </TooltipPanel>\r\n )\r\n}\r\n"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { jsxs, jsx } from "react/jsx-runtime";
|
|
2
|
+
import { forwardRef } from "react";
|
|
3
|
+
import { popoverReset } from "../topLayer.js";
|
|
4
|
+
const TooltipPanel = forwardRef(function TooltipPanel2({ id, title, children, visible, interactive = false, style, onMouseEnter, onMouseLeave }, ref) {
|
|
5
|
+
return /* @__PURE__ */ jsxs(
|
|
6
|
+
"div",
|
|
7
|
+
{
|
|
8
|
+
ref,
|
|
9
|
+
id,
|
|
10
|
+
popover: "manual",
|
|
11
|
+
role: "tooltip",
|
|
12
|
+
hidden: !visible,
|
|
13
|
+
style,
|
|
14
|
+
onMouseEnter,
|
|
15
|
+
onMouseLeave,
|
|
16
|
+
className: `${popoverReset} fixed inset-auto z-[400] max-w-[380px] rounded-xl bg-slate-900 px-3 py-2 text-caption leading-relaxed text-white shadow-modal dark:bg-slate-950 dark:ring-1 dark:ring-slate-700 ${interactive ? "" : "pointer-events-none"}`,
|
|
17
|
+
children: [
|
|
18
|
+
title && /* @__PURE__ */ jsx("div", { className: "mb-1.5 border-b border-slate-700 pb-1.5 text-xs font-bold", children: title }),
|
|
19
|
+
children
|
|
20
|
+
]
|
|
21
|
+
}
|
|
22
|
+
);
|
|
23
|
+
});
|
|
24
|
+
const OFFSET = 10;
|
|
25
|
+
const MARGIN = 12;
|
|
26
|
+
const clamp = (value, size, max) => Math.min(Math.max(MARGIN, value), Math.max(MARGIN, max - MARGIN - size));
|
|
27
|
+
export {
|
|
28
|
+
MARGIN,
|
|
29
|
+
OFFSET,
|
|
30
|
+
clamp,
|
|
31
|
+
TooltipPanel as default
|
|
32
|
+
};
|
|
33
|
+
//# sourceMappingURL=TooltipPanel.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"TooltipPanel.js","sources":["../../../../../../src/v1/components/overlays/Tooltip/TooltipPanel.tsx"],"sourcesContent":["import React, { forwardRef } from 'react'\r\nimport { popoverReset } from '../topLayer'\r\n\r\n/**\r\n * O painel escuro das tooltips (uso interno): o mesmo para a camada `data-tip`\r\n * (TooltipLayer) e para o `Tooltip` com conteúdo rico. Fica na camada de cima\r\n * do browser (`popover`), por cima de uma janela aberta.\r\n */\r\nconst TooltipPanel = forwardRef<\r\n HTMLDivElement,\r\n {\r\n id?: string\r\n title?: React.ReactNode\r\n children: React.ReactNode\r\n visible: boolean\r\n /** Aceita o rato (para se poder clicar em links lá dentro). */\r\n interactive?: boolean\r\n style?: React.CSSProperties\r\n onMouseEnter?: () => void\r\n onMouseLeave?: () => void\r\n }\r\n>(function TooltipPanel({ id, title, children, visible, interactive = false, style, onMouseEnter, onMouseLeave }, ref) {\r\n return (\r\n <div\r\n ref={ref}\r\n id={id}\r\n popover=\"manual\"\r\n role=\"tooltip\"\r\n hidden={!visible}\r\n style={style}\r\n onMouseEnter={onMouseEnter}\r\n onMouseLeave={onMouseLeave}\r\n className={\r\n `${popoverReset} fixed inset-auto z-[400] max-w-[380px] rounded-xl bg-slate-900 px-3 py-2 text-caption leading-relaxed text-white shadow-modal ` +\r\n `dark:bg-slate-950 dark:ring-1 dark:ring-slate-700 ${interactive ? '' : 'pointer-events-none'}`\r\n }\r\n >\r\n {title && <div className=\"mb-1.5 border-b border-slate-700 pb-1.5 text-xs font-bold\">{title}</div>}\r\n {children}\r\n </div>\r\n )\r\n})\r\n\r\nexport default TooltipPanel\r\n\r\n/** Distância ao cursor/elemento e margem mínima ao bordo da janela. */\r\nexport const OFFSET = 10\r\nexport const MARGIN = 12\r\n\r\n/** Encosta o painel dentro da janela. */\r\nexport const clamp = (value: number, size: number, max: number) => Math.min(Math.max(MARGIN, value), Math.max(MARGIN, max - MARGIN - size))\r\n"],"names":["TooltipPanel"],"mappings":";;;AAQA,MAAM,eAAe,WAanB,SAASA,cAAa,EAAE,IAAI,OAAO,UAAU,SAAS,cAAc,OAAO,OAAO,cAAc,aAAA,GAAgB,KAAK;AACrH,SACE;AAAA,IAAC;AAAA,IAAA;AAAA,MACC;AAAA,MACA;AAAA,MACA,SAAQ;AAAA,MACR,MAAK;AAAA,MACL,QAAQ,CAAC;AAAA,MACT;AAAA,MACA;AAAA,MACA;AAAA,MACA,WACE,GAAG,YAAY,oLACsC,cAAc,KAAK,qBAAqB;AAAA,MAG9F,UAAA;AAAA,QAAA,SAAS,oBAAC,OAAA,EAAI,WAAU,6DAA6D,UAAA,OAAM;AAAA,QAC3F;AAAA,MAAA;AAAA,IAAA;AAAA,EAAA;AAGP,CAAC;AAKM,MAAM,SAAS;AACf,MAAM,SAAS;AAGf,MAAM,QAAQ,CAAC,OAAe,MAAc,QAAgB,KAAK,IAAI,KAAK,IAAI,QAAQ,KAAK,GAAG,KAAK,IAAI,QAAQ,MAAM,SAAS,IAAI,CAAC;"}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { useCallback, useLayoutEffect } from "react";
|
|
2
|
+
function useTopLayer(ref, visible) {
|
|
3
|
+
const raise = useCallback(() => {
|
|
4
|
+
const el = ref.current;
|
|
5
|
+
if (!el || typeof el.showPopover !== "function") return;
|
|
6
|
+
try {
|
|
7
|
+
if (el.matches(":popover-open")) el.hidePopover();
|
|
8
|
+
el.showPopover();
|
|
9
|
+
} catch {
|
|
10
|
+
}
|
|
11
|
+
}, [ref]);
|
|
12
|
+
useLayoutEffect(() => {
|
|
13
|
+
const el = ref.current;
|
|
14
|
+
if (!el || typeof el.showPopover !== "function") return;
|
|
15
|
+
try {
|
|
16
|
+
if (visible) raise();
|
|
17
|
+
else if (el.matches(":popover-open")) el.hidePopover();
|
|
18
|
+
} catch {
|
|
19
|
+
}
|
|
20
|
+
}, [visible, raise, ref]);
|
|
21
|
+
return raise;
|
|
22
|
+
}
|
|
23
|
+
const popoverReset = "m-0 border-0 overflow-visible";
|
|
24
|
+
export {
|
|
25
|
+
popoverReset,
|
|
26
|
+
useTopLayer
|
|
27
|
+
};
|
|
28
|
+
//# sourceMappingURL=topLayer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"topLayer.js","sources":["../../../../../src/v1/components/overlays/topLayer.ts"],"sourcesContent":["import { useCallback, useLayoutEffect, type RefObject } from 'react'\r\n\r\n/**\r\n * Põe um elemento na camada de cima do browser (top layer), por cima de um\r\n * Dialog aberto. Só para uso interno (avisos, tooltips): o elemento tem de ter\r\n * `popover=\"manual\"`.\r\n *\r\n * Um `<dialog>` aberto com `showModal()` fica por cima de tudo o que está na\r\n * página, incluindo `position: fixed` com `z-index` alto. Um popover também vai\r\n * para essa camada, e fica por cima de quem lá entrou antes: por isso `raise()`\r\n * volta a mostrá-lo quando é preciso que apareça à frente de uma janela que\r\n * abriu depois.\r\n *\r\n * Em browsers sem popover (e no jsdom dos testes) não faz nada: o elemento\r\n * continua com o seu `z-index`.\r\n */\r\nexport function useTopLayer(ref: RefObject<HTMLElement | null>, visible: boolean) {\r\n const raise = useCallback(() => {\r\n const el = ref.current\r\n if (!el || typeof el.showPopover !== 'function') return\r\n try {\r\n if (el.matches(':popover-open')) el.hidePopover()\r\n el.showPopover()\r\n } catch {\r\n // Elemento desligado do documento entretanto: ignora.\r\n }\r\n }, [ref])\r\n\r\n useLayoutEffect(() => {\r\n const el = ref.current\r\n if (!el || typeof el.showPopover !== 'function') return\r\n try {\r\n if (visible) raise()\r\n else if (el.matches(':popover-open')) el.hidePopover()\r\n } catch {\r\n // idem\r\n }\r\n }, [visible, raise, ref])\r\n\r\n return raise\r\n}\r\n\r\n/**\r\n * Anula a margem, a borda e o scroll que o browser dá aos popovers. O fundo e o\r\n * espaçamento ficam com cada componente: pôr aqui `bg-transparent`/`p-0` junto\r\n * com o `bg-…`/`px-…` do componente deixava o resultado à mercê da ordem do CSS\r\n * (numa app com Tailwind próprio, a tooltip ficava transparente).\r\n */\r\nexport const popoverReset = 'm-0 border-0 overflow-visible'\r\n"],"names":[],"mappings":";AAgBO,SAAS,YAAY,KAAoC,SAAkB;AAChF,QAAM,QAAQ,YAAY,MAAM;AAC9B,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,MAAM,OAAO,GAAG,gBAAgB,WAAY;AACjD,QAAI;AACF,UAAI,GAAG,QAAQ,eAAe,MAAM,YAAA;AACpC,SAAG,YAAA;AAAA,IACL,QAAQ;AAAA,IAER;AAAA,EACF,GAAG,CAAC,GAAG,CAAC;AAER,kBAAgB,MAAM;AACpB,UAAM,KAAK,IAAI;AACf,QAAI,CAAC,MAAM,OAAO,GAAG,gBAAgB,WAAY;AACjD,QAAI;AACF,UAAI,QAAS,OAAA;AAAA,eACJ,GAAG,QAAQ,eAAe,MAAM,YAAA;AAAA,IAC3C,QAAQ;AAAA,IAER;AAAA,EACF,GAAG,CAAC,SAAS,OAAO,GAAG,CAAC;AAExB,SAAO;AACT;AAQO,MAAM,eAAe;"}
|