@1doc/1ds-react 2.13.0 → 2.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/dist/components/Button/Button.css +6 -1
- package/dist/components/Button/Button.d.ts +11 -2
- package/dist/components/Button/Button.js +15 -3
- package/dist/components/Counter/Counter.css +43 -0
- package/dist/components/Counter/Counter.d.ts +57 -0
- package/dist/components/Counter/Counter.js +99 -0
- package/dist/components/Dropdown/Dropdown.css +2 -0
- package/dist/components/Dropdown/Dropdown.d.ts +9 -0
- package/dist/components/Dropdown/Dropdown.js +20 -2
- package/dist/components/List/List.css +47 -0
- package/dist/components/List/ListItem.d.ts +10 -0
- package/dist/components/List/ListItem.js +1 -1
- package/dist/components/StatCard/StatCard.css +57 -2
- package/dist/components/StatCard/StatCard.d.ts +12 -6
- package/dist/components/StatCard/StatCard.js +6 -2
- package/dist/components/Tooltip/Tooltip.css +3 -0
- package/dist/components/Tooltip/Tooltip.js +33 -2
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/teste/PedidosMatheus.test.d.ts +1 -0
- package/dist/teste/PedidosMatheus.test.js +91 -0
- package/dist/teste/StatCardTamanhoSm.test.js +8 -0
- package/package.json +3 -3
- package/src/index.ts +2 -0
- package/src/teste/PedidosMatheus.test.tsx +129 -0
- package/src/teste/StatCardTamanhoSm.test.tsx +9 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# @1doc/1ds-react
|
|
2
2
|
|
|
3
|
+
## 2.14.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- aca81ec: Ajustes pedidos na migração de produto para o 1DS, e um componente novo.
|
|
8
|
+
|
|
9
|
+
- **Counter** (novo): número que conta até o valor, para KPI de painel e total de páginas. Formato do `Intl.NumberFormat` (pt-BR por padrão), `prefix`/`suffix`/`decimals`, largura do valor final reservada, leitor de tela ouvindo só o valor final e respeito a `prefers-reduced-motion`.
|
|
10
|
+
- **ListItem**: slot `actions`, renderizado no `li` ao lado do elemento clicável (o ⋮ de menu deixa de ficar dentro do link). No Vue, o ListItem com `li` passa a ter raiz única, então funciona dentro de `TransitionGroup`.
|
|
11
|
+
- **Tooltip**: a cópia escondida do texto não escapa mais de área com rolagem (o wrapper ganhou `position: relative`), e a seta acompanha o gatilho quando o balão é empurrado para caber perto da borda.
|
|
12
|
+
- **Dropdown**: `href`, `target` e `rel` no `DropdownItem`. Com `href`, o item é um `<a role="menuitem">` (Ctrl+clique e nova aba funcionam), e o `onSelect` continua sendo chamado.
|
|
13
|
+
- **Button**: `href`, `target` e `rel`. Com `href`, o Button é um `<a>` com a mesma aparência; desabilitado, volta a ser `<button disabled>`. O hover do `variant="icon"` virou uma camada translúcida da cor do ícone: no fundo claro o cinza é o mesmo de antes, e sobre fundo colorido não vira mais um quadrado branco.
|
|
14
|
+
- **StatCard**: `secondaryTone` (cor da segunda medida), `loading` (os números viram Skeleton e o cartão ganha `aria-busy`) e `value` aceitando um nó, como um `Counter` (slot `value` no Vue).
|
|
15
|
+
|
|
16
|
+
## 2.13.1
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- 1a927c5: StatCard: a segunda medida (`secondaryValue`) passa a ter o mesmo tamanho e peso do número principal, em todas as combinações (`md`, `sm` e `labelPosition="top"`). Com o rótulo no topo, o `secondaryLabel` fica na mesma linha da `description` ("Geral" ao lado de "Meu Setor"), em vez de colado embaixo do número.
|
|
21
|
+
|
|
3
22
|
## 2.13.0
|
|
4
23
|
|
|
5
24
|
### Minor Changes
|
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
line-height: 1;
|
|
23
23
|
white-space: nowrap;
|
|
24
24
|
text-decoration: none;
|
|
25
|
+
box-sizing: border-box; /* com href o Button é <a>, que nasce content-box */
|
|
25
26
|
|
|
26
27
|
transition:
|
|
27
28
|
background-color var(--umds-semantic-motion-duration-fast) var(--umds-semantic-motion-easing-standard),
|
|
@@ -210,8 +211,12 @@
|
|
|
210
211
|
.umds-button--icon.umds-button--md { width: var(--umds-primitive-spacing-sm); } /* 40px */
|
|
211
212
|
.umds-button--icon.umds-button--lg { width: var(--umds-primitive-spacing-md); } /* 48px */
|
|
212
213
|
|
|
214
|
+
/* Camada de estado translúcida, da cor do próprio ícone: funciona sobre
|
|
215
|
+
qualquer fundo (o ⋮ num item azul selecionado não vira um quadrado branco).
|
|
216
|
+
12% de neutral-dark-down (#565656) sobre o branco dá #EBEBEB, o mesmo
|
|
217
|
+
neutral-light-medium de antes: no fundo claro o visual não muda. */
|
|
213
218
|
.umds-button--icon:hover:not(:disabled) {
|
|
214
|
-
background-color:
|
|
219
|
+
background-color: color-mix(in srgb, currentColor 12%, transparent);
|
|
215
220
|
}
|
|
216
221
|
|
|
217
222
|
.umds-button--icon:disabled {
|
|
@@ -22,8 +22,11 @@ import './Button.css';
|
|
|
22
22
|
* @property {boolean} autoFocus - Foca o botão automaticamente ao montar
|
|
23
23
|
* @property {number} tabIndex - Ordem de tabulação nativa
|
|
24
24
|
* @property {object} style - Estilos inline aplicados ao botão (objeto no React, string/objeto no Vue)
|
|
25
|
-
* @
|
|
26
|
-
* @
|
|
25
|
+
* @property {string} href - Botão que NAVEGA: com `href` o Button vira `<a>` com a mesma aparência (Ctrl+clique e "abrir em nova aba" funcionam). Desabilitado, volta a ser `<button disabled>`.
|
|
26
|
+
* @property {string} target - Destino do link (`_blank` ganha `rel="noopener noreferrer"`)
|
|
27
|
+
* @property {string} rel - Relação do link; sobrescreve o padrão do `target`
|
|
28
|
+
* @quandoUsar AÇÃO que muda estado: salvar, enviar, abrir modal, confirmar. Use `variant="icon"` para ação só com ícone (sempre com `ariaLabel`). Para navegação com cara de botão (o "Ver todos" de um rodapé de card), passe `href`: sai um `<a>` de verdade.
|
|
29
|
+
* @quandoNaoUsar Para navegar com cara de texto, no meio de uma frase, use `Link`. Nunca navegue por `onClick`: sem `href` não há Ctrl+clique nem "abrir em nova aba".
|
|
27
30
|
*/
|
|
28
31
|
export interface ButtonProps {
|
|
29
32
|
variant?: "primary" | "secondary" | "neutral" | "neutral-text" | "text" | "negative" | "negative-outlined" | "icon";
|
|
@@ -48,6 +51,12 @@ export interface ButtonProps {
|
|
|
48
51
|
form?: string;
|
|
49
52
|
autoFocus?: boolean;
|
|
50
53
|
tabIndex?: number;
|
|
54
|
+
/** Botão que navega: vira `<a>` com a mesma aparência. Desabilitado, volta a ser `<button>`. */
|
|
55
|
+
href?: string;
|
|
56
|
+
/** Destino do link. `_blank` ganha `rel="noopener noreferrer"`. */
|
|
57
|
+
target?: string;
|
|
58
|
+
/** Relação do link. Sobrescreve o padrão do `target`. */
|
|
59
|
+
rel?: string;
|
|
51
60
|
}
|
|
52
61
|
import { IconProp } from "../Icon/Icon";
|
|
53
62
|
declare function Button(props: ButtonProps): import("react/jsx-runtime").JSX.Element;
|
|
@@ -63,15 +63,27 @@ function Button(props) {
|
|
|
63
63
|
function endIconIsIcon() {
|
|
64
64
|
return isIconValue(props.endIcon);
|
|
65
65
|
}
|
|
66
|
+
function isLink() {
|
|
67
|
+
return !!props.href && !props.disabled;
|
|
68
|
+
}
|
|
69
|
+
function relValue() {
|
|
70
|
+
if (props.rel)
|
|
71
|
+
return props.rel;
|
|
72
|
+
return props.target === "_blank" ? "noopener noreferrer" : undefined;
|
|
73
|
+
}
|
|
66
74
|
function handleClick(event) {
|
|
67
75
|
// Clique bloqueado durante o carregamento — evita dupla submissão
|
|
68
|
-
// sem mudar o visual (o botão não fica disabled).
|
|
69
|
-
|
|
76
|
+
// sem mudar o visual (o botão não fica disabled). No `<a>`, segura
|
|
77
|
+
// também a navegação.
|
|
78
|
+
if (props.loading) {
|
|
79
|
+
if (event && event.preventDefault)
|
|
80
|
+
event.preventDefault();
|
|
70
81
|
return;
|
|
82
|
+
}
|
|
71
83
|
if (props.onClick) {
|
|
72
84
|
props.onClick(event);
|
|
73
85
|
}
|
|
74
86
|
}
|
|
75
|
-
return (_jsxs("button", { type: props.type || "button", disabled: props.disabled, id: props.id, name: props.name, value: props.value, form: props.form, autoFocus: props.autoFocus || undefined, tabIndex: props.tabIndex, "aria-label": props.ariaLabel, "aria-busy": props.loading, onClick: (event) => handleClick(event), className: classes(), children: [showLoadingAtStart() ? (_jsx("span", { "aria-hidden": "true", className: "umds-button__loading", children: _jsx(Icon, { variant: "inherit", icon: props.loadingIcon || faRefresh, size: iconSize() }) })) : null, showStartIcon() ? (_jsx(_Fragment, { children: startIconIsIcon() ? (_jsx(Icon, { variant: "inherit", icon: props.startIcon, size: iconSize() })) : (_jsx(_Fragment, { children: _jsx(_Fragment, { children: props.startIcon }) })) })) : null, showLoadingAtCenter() ? (_jsx("span", { "aria-hidden": "true", className: "umds-button__loading", children: _jsx(Icon, { variant: "inherit", icon: props.loadingIcon || faRefresh, size: iconSize() }) })) : null, showLoadingAtCenter() ? (_jsx("span", { className: "umds-button__visually-hidden", children: props.children })) : null, showChildren() ? _jsx(_Fragment, { children: props.children }) : null, showEndIcon() ? (_jsx(_Fragment, { children: endIconIsIcon() ? (_jsx(Icon, { variant: "inherit", icon: props.endIcon, size: iconSize() })) : (_jsx(_Fragment, { children: _jsx(_Fragment, { children: props.endIcon }) })) })) : null, showLoadingAtEnd() ? (_jsx("span", { "aria-hidden": "true", className: "umds-button__loading", children: _jsx(Icon, { variant: "inherit", icon: props.loadingIcon || faRefresh, size: iconSize() }) })) : null] }));
|
|
87
|
+
return (_jsx(_Fragment, { children: isLink() ? (_jsx(_Fragment, { children: _jsxs("a", { href: props.href, target: props.target, rel: relValue(), id: props.id, tabIndex: props.tabIndex, "aria-label": props.ariaLabel, "aria-busy": props.loading, onClick: (event) => handleClick(event), className: classes(), children: [showLoadingAtStart() ? (_jsx("span", { "aria-hidden": "true", className: "umds-button__loading", children: _jsx(Icon, { variant: "inherit", icon: props.loadingIcon || faRefresh, size: iconSize() }) })) : null, showStartIcon() ? (_jsx(_Fragment, { children: startIconIsIcon() ? (_jsx(Icon, { variant: "inherit", icon: props.startIcon, size: iconSize() })) : (_jsx(_Fragment, { children: _jsx(_Fragment, { children: props.startIcon }) })) })) : null, showLoadingAtCenter() ? (_jsx("span", { "aria-hidden": "true", className: "umds-button__loading", children: _jsx(Icon, { variant: "inherit", icon: props.loadingIcon || faRefresh, size: iconSize() }) })) : null, showLoadingAtCenter() ? (_jsx("span", { className: "umds-button__visually-hidden", children: props.children })) : null, showChildren() ? _jsx(_Fragment, { children: props.children }) : null, showEndIcon() ? (_jsx(_Fragment, { children: endIconIsIcon() ? (_jsx(Icon, { variant: "inherit", icon: props.endIcon, size: iconSize() })) : (_jsx(_Fragment, { children: _jsx(_Fragment, { children: props.endIcon }) })) })) : null, showLoadingAtEnd() ? (_jsx("span", { "aria-hidden": "true", className: "umds-button__loading", children: _jsx(Icon, { variant: "inherit", icon: props.loadingIcon || faRefresh, size: iconSize() }) })) : null] }) })) : (_jsx(_Fragment, { children: _jsxs("button", { type: props.type || "button", disabled: props.disabled, id: props.id, name: props.name, value: props.value, form: props.form, autoFocus: props.autoFocus || undefined, tabIndex: props.tabIndex, "aria-label": props.ariaLabel, "aria-busy": props.loading, onClick: (event) => handleClick(event), className: classes(), children: [showLoadingAtStart() ? (_jsx("span", { "aria-hidden": "true", className: "umds-button__loading", children: _jsx(Icon, { variant: "inherit", icon: props.loadingIcon || faRefresh, size: iconSize() }) })) : null, showStartIcon() ? (_jsx(_Fragment, { children: startIconIsIcon() ? (_jsx(Icon, { variant: "inherit", icon: props.startIcon, size: iconSize() })) : (_jsx(_Fragment, { children: _jsx(_Fragment, { children: props.startIcon }) })) })) : null, showLoadingAtCenter() ? (_jsx("span", { "aria-hidden": "true", className: "umds-button__loading", children: _jsx(Icon, { variant: "inherit", icon: props.loadingIcon || faRefresh, size: iconSize() }) })) : null, showLoadingAtCenter() ? (_jsx("span", { className: "umds-button__visually-hidden", children: props.children })) : null, showChildren() ? _jsx(_Fragment, { children: props.children }) : null, showEndIcon() ? (_jsx(_Fragment, { children: endIconIsIcon() ? (_jsx(Icon, { variant: "inherit", icon: props.endIcon, size: iconSize() })) : (_jsx(_Fragment, { children: _jsx(_Fragment, { children: props.endIcon }) })) })) : null, showLoadingAtEnd() ? (_jsx("span", { "aria-hidden": "true", className: "umds-button__loading", children: _jsx(Icon, { variant: "inherit", icon: props.loadingIcon || faRefresh, size: iconSize() }) })) : null] }) })) }));
|
|
76
88
|
}
|
|
77
89
|
export default Button;
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/* ─────────────────────────────────────────────
|
|
2
|
+
COUNTER
|
|
3
|
+
Número que conta até o valor. Herda fonte, tamanho e cor de onde está.
|
|
4
|
+
A reserva (cópia invisível do valor final) e o número que corre ocupam a
|
|
5
|
+
mesma célula da grade: a largura é a do valor final desde o primeiro quadro.
|
|
6
|
+
───────────────────────────────────────────── */
|
|
7
|
+
|
|
8
|
+
.umds-counter {
|
|
9
|
+
/* Contém a cópia escondida (__leitura, absolute): sem isso ela escapa do
|
|
10
|
+
overflow de uma área que rola (o mesmo bug corrigido no Tooltip). */
|
|
11
|
+
position: relative;
|
|
12
|
+
display: inline-grid;
|
|
13
|
+
font-variant-numeric: tabular-nums;
|
|
14
|
+
white-space: nowrap;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
.umds-counter__reserva,
|
|
18
|
+
.umds-counter__numero {
|
|
19
|
+
grid-area: 1 / 1;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
.umds-counter__reserva {
|
|
23
|
+
visibility: hidden;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
.umds-counter__numero {
|
|
27
|
+
justify-self: end;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/* Valor final para o leitor de tela. Receita visually-hidden; os 1px não são
|
|
31
|
+
medida de design. */
|
|
32
|
+
.umds-counter__leitura {
|
|
33
|
+
position: absolute;
|
|
34
|
+
width: 1px;
|
|
35
|
+
height: 1px;
|
|
36
|
+
margin: -1px;
|
|
37
|
+
padding: 0;
|
|
38
|
+
border: 0;
|
|
39
|
+
overflow: hidden;
|
|
40
|
+
clip: rect(0, 0, 0, 0);
|
|
41
|
+
clip-path: inset(50%);
|
|
42
|
+
white-space: nowrap;
|
|
43
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import './Counter.css';
|
|
2
|
+
/**
|
|
3
|
+
* Counter — número que conta até o valor.
|
|
4
|
+
*
|
|
5
|
+
* Ao aparecer, o número sobe de `from` (0) até `value`; quando `value` muda,
|
|
6
|
+
* conta do valor atual até o novo. É o número do painel ("1.284 processos") e o
|
|
7
|
+
* total de páginas da caixa de entrada. Herda fonte, tamanho e cor de onde
|
|
8
|
+
* está, então serve dentro de um título, de um `StatCard` ou no meio do texto.
|
|
9
|
+
*
|
|
10
|
+
* Três decisões que valem saber:
|
|
11
|
+
*
|
|
12
|
+
* - **O leitor de tela lê só o valor final.** O número que corre é
|
|
13
|
+
* `aria-hidden`; uma cópia escondida com o valor final é o que se anuncia.
|
|
14
|
+
* Ninguém ouve "1, 37, 210, 1.284".
|
|
15
|
+
* - **A largura não pula.** Uma cópia invisível do valor final reserva o
|
|
16
|
+
* espaço, e os algarismos são tabulares: o texto em volta não dança enquanto
|
|
17
|
+
* o número conta.
|
|
18
|
+
* - **Movimento reduzido é respeitado.** Com `prefers-reduced-motion` (ou
|
|
19
|
+
* `disableAnimation`) o número aparece direto no valor final.
|
|
20
|
+
*
|
|
21
|
+
* O formato é o do `Intl.NumberFormat` em `locale` (pt-BR: "1.284", "22,31").
|
|
22
|
+
* As casas decimais seguem as do `value`, a menos que `decimals` diga outra
|
|
23
|
+
* coisa.
|
|
24
|
+
* @property {number} value - Valor final (número ou texto numérico)
|
|
25
|
+
* @property {number} from - De onde a contagem começa ao montar (padrão 0)
|
|
26
|
+
* @property {number} duration - Duração da contagem em ms (padrão 1000)
|
|
27
|
+
* @property {number} decimals - Casas decimais (padrão: as do `value`)
|
|
28
|
+
* @property {string} locale - Idioma do formato (padrão "pt-BR")
|
|
29
|
+
* @property {string} prefix - Texto antes do número ("R$ ")
|
|
30
|
+
* @property {string} suffix - Texto depois do número ("%", " páginas")
|
|
31
|
+
* @property {boolean} disableAnimation - Mostra o valor final direto, sem contar
|
|
32
|
+
* @property {string} className - Classes CSS adicionais
|
|
33
|
+
* @quandoUsar Número de destaque que merece atenção ao aparecer: KPI de painel, total de páginas ou de itens de uma caixa de entrada, contadores de resultado.
|
|
34
|
+
* @quandoNaoUsar Para contagem pequena presa a outro elemento (não lidos de uma aba), use `Badge`. Para número que muda o tempo todo (cronômetro, campo editável), mostre o texto direto: a animação a cada mudança cansa. Em tabela (`DataTable`), número não anima.
|
|
35
|
+
*/
|
|
36
|
+
export interface CounterProps {
|
|
37
|
+
/** Valor final. Aceita número ou texto numérico ("1284", "22.31"). */
|
|
38
|
+
value: number | string;
|
|
39
|
+
/** De onde a contagem começa ao montar. @default 0 */
|
|
40
|
+
from?: number;
|
|
41
|
+
/** Duração da contagem, em milissegundos. @default 1000 */
|
|
42
|
+
duration?: number;
|
|
43
|
+
/** Casas decimais. Sem a prop, as do `value`. */
|
|
44
|
+
decimals?: number;
|
|
45
|
+
/** Idioma do formato do número. @default 'pt-BR' */
|
|
46
|
+
locale?: string;
|
|
47
|
+
/** Texto antes do número ("R$ "). */
|
|
48
|
+
prefix?: string;
|
|
49
|
+
/** Texto depois do número ("%", " páginas"). */
|
|
50
|
+
suffix?: string;
|
|
51
|
+
/** Mostra o valor final direto, sem contar. @default false */
|
|
52
|
+
disableAnimation?: boolean;
|
|
53
|
+
/** Classes CSS adicionais. */
|
|
54
|
+
className?: string;
|
|
55
|
+
}
|
|
56
|
+
declare function Counter(props: CounterProps): import("react/jsx-runtime").JSX.Element;
|
|
57
|
+
export default Counter;
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
|
+
import './Counter.css';
|
|
4
|
+
import { useState, useRef, useEffect } from "react";
|
|
5
|
+
function Counter(props) {
|
|
6
|
+
const quadroRef = useRef(null);
|
|
7
|
+
const ultimoRef = useRef(null);
|
|
8
|
+
const alvoRef = useRef(null);
|
|
9
|
+
const [exibido, setExibido] = useState(() => null);
|
|
10
|
+
function alvo() {
|
|
11
|
+
const numero = Number(props.value);
|
|
12
|
+
return isFinite(numero) ? numero : 0;
|
|
13
|
+
}
|
|
14
|
+
function casas() {
|
|
15
|
+
if (typeof props.decimals === "number")
|
|
16
|
+
return props.decimals;
|
|
17
|
+
const texto = String(props.value);
|
|
18
|
+
const ponto = texto.indexOf(".");
|
|
19
|
+
return ponto < 0 ? 0 : Math.min(texto.length - ponto - 1, 4);
|
|
20
|
+
}
|
|
21
|
+
function formatar(numero) {
|
|
22
|
+
const digitos = casas();
|
|
23
|
+
const formatado = new Intl.NumberFormat(props.locale || "pt-BR", {
|
|
24
|
+
minimumFractionDigits: digitos,
|
|
25
|
+
maximumFractionDigits: digitos,
|
|
26
|
+
}).format(numero);
|
|
27
|
+
return (props.prefix || "") + formatado + (props.suffix || "");
|
|
28
|
+
}
|
|
29
|
+
function semAnimacao() {
|
|
30
|
+
if (props.disableAnimation)
|
|
31
|
+
return true;
|
|
32
|
+
if (typeof window === "undefined" || !window.matchMedia)
|
|
33
|
+
return false;
|
|
34
|
+
return window.matchMedia("(prefers-reduced-motion: reduce)").matches;
|
|
35
|
+
}
|
|
36
|
+
function parar() {
|
|
37
|
+
if (quadroRef.current !== null && typeof window !== "undefined") {
|
|
38
|
+
window.cancelAnimationFrame(quadroRef.current);
|
|
39
|
+
}
|
|
40
|
+
quadroRef.current = null;
|
|
41
|
+
}
|
|
42
|
+
function animar(de, para) {
|
|
43
|
+
parar();
|
|
44
|
+
const duracao = typeof props.duration === "number" ? props.duration : 1000;
|
|
45
|
+
if (semAnimacao() ||
|
|
46
|
+
duracao <= 0 ||
|
|
47
|
+
de === para ||
|
|
48
|
+
typeof window === "undefined") {
|
|
49
|
+
ultimoRef.current = para;
|
|
50
|
+
setExibido(para);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const inicio = performance.now();
|
|
54
|
+
const passo = (agora) => {
|
|
55
|
+
const progresso = Math.min(1, (agora - inicio) / duracao);
|
|
56
|
+
// ease-out cúbico: rápido no começo, assenta devagar no valor.
|
|
57
|
+
const suave = 1 - Math.pow(1 - progresso, 3);
|
|
58
|
+
const atual = progresso >= 1 ? para : de + (para - de) * suave;
|
|
59
|
+
ultimoRef.current = atual;
|
|
60
|
+
setExibido(atual);
|
|
61
|
+
quadroRef.current =
|
|
62
|
+
progresso < 1 ? window.requestAnimationFrame(passo) : null;
|
|
63
|
+
};
|
|
64
|
+
quadroRef.current = window.requestAnimationFrame(passo);
|
|
65
|
+
}
|
|
66
|
+
function textoNaTela() {
|
|
67
|
+
if (exibido !== null)
|
|
68
|
+
return formatar(exibido);
|
|
69
|
+
if (props.disableAnimation)
|
|
70
|
+
return formatar(alvo());
|
|
71
|
+
return formatar(typeof props.from === "number" ? props.from : 0);
|
|
72
|
+
}
|
|
73
|
+
function textoFinal() {
|
|
74
|
+
return formatar(alvo());
|
|
75
|
+
}
|
|
76
|
+
function classes() {
|
|
77
|
+
return ["umds-counter", props.className].filter(Boolean).join(" ");
|
|
78
|
+
}
|
|
79
|
+
useEffect(() => {
|
|
80
|
+
const para = alvo();
|
|
81
|
+
alvoRef.current = para;
|
|
82
|
+
animar(typeof props.from === "number" ? props.from : 0, para);
|
|
83
|
+
}, []);
|
|
84
|
+
useEffect(() => {
|
|
85
|
+
const para = alvo();
|
|
86
|
+
// O React roda este efeito também na montagem: o onMount já cuidou dela.
|
|
87
|
+
if (ultimoRef.current === null || alvoRef.current === para)
|
|
88
|
+
return;
|
|
89
|
+
alvoRef.current = para;
|
|
90
|
+
animar(ultimoRef.current, para);
|
|
91
|
+
}, [props.value]);
|
|
92
|
+
useEffect(() => {
|
|
93
|
+
return () => {
|
|
94
|
+
parar();
|
|
95
|
+
};
|
|
96
|
+
}, []);
|
|
97
|
+
return (_jsxs("span", { className: classes(), children: [_jsx("span", { className: "umds-counter__reserva", "aria-hidden": "true", children: textoFinal() }), _jsx("span", { className: "umds-counter__numero", "aria-hidden": "true", children: textoNaTela() }), _jsx("span", { className: "umds-counter__leitura", children: textoFinal() })] }));
|
|
98
|
+
}
|
|
99
|
+
export default Counter;
|
|
@@ -55,10 +55,12 @@
|
|
|
55
55
|
align-items: center;
|
|
56
56
|
gap: var(--umds-primitive-spacing-nano); /* 8px */
|
|
57
57
|
width: 100%;
|
|
58
|
+
box-sizing: border-box; /* o item com href é <a>, que nasce content-box */
|
|
58
59
|
padding: var(--umds-primitive-spacing-nano) var(--umds-primitive-spacing-xxxs); /* 8px 16px */
|
|
59
60
|
background: none;
|
|
60
61
|
border: none;
|
|
61
62
|
text-align: left;
|
|
63
|
+
text-decoration: none;
|
|
62
64
|
font-family: var(--umds-semantic-font-family-base);
|
|
63
65
|
font-size: var(--umds-primitive-font-size-xxs); /* 14px */
|
|
64
66
|
font-weight: 400;
|
|
@@ -7,6 +7,9 @@ import './Dropdown.css';
|
|
|
7
7
|
* @property {boolean} disabled - Desabilita o item (não selecionável).
|
|
8
8
|
* @property {boolean} danger - Aplica o estilo destrutivo (vermelho).
|
|
9
9
|
* @property {boolean} dividerAfter - Adiciona um divisor abaixo do item.
|
|
10
|
+
* @property {string} href - Item que navega: vira `<a>` de verdade (Ctrl+clique e "abrir em nova aba" funcionam). O `onSelect` continua sendo chamado.
|
|
11
|
+
* @property {string} target - Destino do link (`_blank` ganha `rel="noopener noreferrer"`).
|
|
12
|
+
* @property {string} rel - Relação do link; sobrescreve o padrão do `target`.
|
|
10
13
|
*/
|
|
11
14
|
export interface DropdownItem {
|
|
12
15
|
key: string;
|
|
@@ -15,6 +18,12 @@ export interface DropdownItem {
|
|
|
15
18
|
disabled?: boolean;
|
|
16
19
|
danger?: boolean;
|
|
17
20
|
dividerAfter?: boolean;
|
|
21
|
+
/** Item que navega: vira `<a>` (Ctrl+clique e nova aba funcionam). Desabilitado, volta a ser botão. */
|
|
22
|
+
href?: string;
|
|
23
|
+
/** Destino do link. `_blank` ganha `rel="noopener noreferrer"`. */
|
|
24
|
+
target?: string;
|
|
25
|
+
/** Relação do link. Sobrescreve o padrão do `target`. */
|
|
26
|
+
rel?: string;
|
|
18
27
|
}
|
|
19
28
|
/**
|
|
20
29
|
* Dropdown component — menu de ações ancorado a um trigger.
|
|
@@ -302,8 +302,21 @@ function Dropdown(props) {
|
|
|
302
302
|
activateIndex(nextEnabledIndex(total, -1));
|
|
303
303
|
}
|
|
304
304
|
else if (key === "Enter" || key === " ") {
|
|
305
|
-
event.preventDefault();
|
|
306
305
|
const item = itemList()[focusedIndex()];
|
|
306
|
+
// Item com href é <a>: o Enter fica com o navegador, que dispara o
|
|
307
|
+
// clique do link (e respeita Ctrl/Shift para nova aba/janela). O
|
|
308
|
+
// onClick do <a> chama o handleSelect. Espaço não ativa link no
|
|
309
|
+
// navegador, então vira um clique de verdade.
|
|
310
|
+
if (item && item.href && !item.disabled) {
|
|
311
|
+
if (key === " ") {
|
|
312
|
+
event.preventDefault();
|
|
313
|
+
const alvo = event.target;
|
|
314
|
+
if (alvo && alvo.click)
|
|
315
|
+
alvo.click();
|
|
316
|
+
}
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
event.preventDefault();
|
|
307
320
|
if (item)
|
|
308
321
|
handleSelect(item);
|
|
309
322
|
}
|
|
@@ -312,6 +325,11 @@ function Dropdown(props) {
|
|
|
312
325
|
close();
|
|
313
326
|
}
|
|
314
327
|
}
|
|
328
|
+
function relDoItem(item) {
|
|
329
|
+
if (item.rel)
|
|
330
|
+
return item.rel;
|
|
331
|
+
return item.target === "_blank" ? "noopener noreferrer" : undefined;
|
|
332
|
+
}
|
|
315
333
|
function itemClasses(item) {
|
|
316
334
|
return [
|
|
317
335
|
"umds-dropdown__item",
|
|
@@ -370,6 +388,6 @@ function Dropdown(props) {
|
|
|
370
388
|
}, []);
|
|
371
389
|
return (_jsxs("span", { ref: triggerRef, className: triggerClasses(), style: {
|
|
372
390
|
display: "inline-flex",
|
|
373
|
-
}, children: [_jsx("span", { className: "umds-dropdown__trigger", ref: triggerButtonRef, role: triggerIsInteractive ? undefined : "button", tabIndex: triggerIsInteractive ? undefined : 0, "aria-haspopup": triggerIsInteractive ? undefined : "menu", "aria-expanded": triggerIsInteractive ? undefined : ariaExpandedText(), onClick: (event) => toggle(), onKeyDown: (event) => handleTriggerKeydown(event), children: _jsx(_Fragment, { children: props.children }) }), _jsx(Portal, { children: mounted ? (_jsx("span", { className: "umds-dropdown__floating", ref: floatingRef, style: floatingStyle(), children: _jsx("ul", { className: "umds-dropdown__menu", role: "menu", ref: menuRef, "aria-label": props.ariaLabel, tabIndex: -1, style: menuStyle(), onKeyDown: (event) => handleMenuKeydown(event), children: itemList()?.map((item, index) => (_jsx("li", { role: "none", children: _jsxs("button", { type: "button", role: "menuitem", className: itemClasses(item), tabIndex: index === activeIndex ? 0 : -1, "data-active": index === activeIndex, disabled: item.disabled, onClick: (event) => handleSelect(item), children: [item.icon ? (_jsx("span", { className: "umds-dropdown__item-icon", "aria-hidden": "true", children: _jsx(Icon, { size: "sm", variant: "inherit", icon: item.icon }) })) : null, _jsx("span", { className: "umds-dropdown__item-label", children: item.label })] }) }, item.key))) }) })) : null })] }));
|
|
391
|
+
}, children: [_jsx("span", { className: "umds-dropdown__trigger", ref: triggerButtonRef, role: triggerIsInteractive ? undefined : "button", tabIndex: triggerIsInteractive ? undefined : 0, "aria-haspopup": triggerIsInteractive ? undefined : "menu", "aria-expanded": triggerIsInteractive ? undefined : ariaExpandedText(), onClick: (event) => toggle(), onKeyDown: (event) => handleTriggerKeydown(event), children: _jsx(_Fragment, { children: props.children }) }), _jsx(Portal, { children: mounted ? (_jsx("span", { className: "umds-dropdown__floating", ref: floatingRef, style: floatingStyle(), children: _jsx("ul", { className: "umds-dropdown__menu", role: "menu", ref: menuRef, "aria-label": props.ariaLabel, tabIndex: -1, style: menuStyle(), onKeyDown: (event) => handleMenuKeydown(event), children: itemList()?.map((item, index) => (_jsx("li", { role: "none", children: item.href && !item.disabled ? (_jsxs("a", { role: "menuitem", href: item.href, target: item.target, rel: relDoItem(item), className: itemClasses(item), tabIndex: index === activeIndex ? 0 : -1, "data-active": index === activeIndex, onClick: (event) => handleSelect(item), children: [item.icon ? (_jsx("span", { className: "umds-dropdown__item-icon", "aria-hidden": "true", children: _jsx(Icon, { size: "sm", variant: "inherit", icon: item.icon }) })) : null, _jsx("span", { className: "umds-dropdown__item-label", children: item.label })] })) : (_jsx(_Fragment, { children: _jsxs("button", { type: "button", role: "menuitem", className: itemClasses(item), tabIndex: index === activeIndex ? 0 : -1, "data-active": index === activeIndex, disabled: item.disabled, onClick: (event) => handleSelect(item), children: [item.icon ? (_jsx("span", { className: "umds-dropdown__item-icon", "aria-hidden": "true", children: _jsx(Icon, { size: "sm", variant: "inherit", icon: item.icon }) })) : null, _jsx("span", { className: "umds-dropdown__item-label", children: item.label })] }) })) }, item.key))) }) })) : null })] }));
|
|
374
392
|
}
|
|
375
393
|
export default Dropdown;
|
|
@@ -134,3 +134,50 @@ ol.umds-list {
|
|
|
134
134
|
display: block;
|
|
135
135
|
color: var(--umds-semantic-color-neutral-dark-down);
|
|
136
136
|
}
|
|
137
|
+
|
|
138
|
+
/* ── Ações da linha (slot `actions`) ──
|
|
139
|
+
O controle fica no `li`, AO LADO do elemento clicável: clicar no ⋮ não
|
|
140
|
+
aciona a linha, e não há botão dentro de link. O `li` vira a linha visual
|
|
141
|
+
(flex), e o hover e a seleção passam do elemento para o `li`, para o fundo
|
|
142
|
+
cobrir as ações também. */
|
|
143
|
+
.umds-list__item:has(> .umds-list-item__actions) {
|
|
144
|
+
display: flex;
|
|
145
|
+
align-items: center;
|
|
146
|
+
gap: var(--umds-primitive-spacing-quarck); /* 4px */
|
|
147
|
+
padding-right: var(--umds-primitive-spacing-nano); /* 8px */
|
|
148
|
+
border-radius: var(--umds-primitive-border-radius-sm); /* 8px */
|
|
149
|
+
transition: background-color var(--umds-semantic-motion-duration-fast) var(--umds-semantic-motion-easing-standard);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
.umds-list__item:has(> .umds-list-item__actions) > .umds-list-item {
|
|
153
|
+
flex: 1 1 auto;
|
|
154
|
+
width: auto;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.umds-list__item:has(> .umds-list-item__actions) > .umds-list-item,
|
|
158
|
+
.umds-list__item:has(> .umds-list-item__actions) > .umds-list-item.umds-list-item--interactive:hover {
|
|
159
|
+
background: transparent;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.umds-list__item:has(> .umds-list-item--interactive:not(.umds-list-item--disabled)):has(> .umds-list-item__actions):hover {
|
|
163
|
+
background: var(--umds-semantic-color-background-light);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
.umds-list__item:has(> .umds-list-item--selected):has(> .umds-list-item__actions) {
|
|
167
|
+
background: var(--umds-semantic-color-primary-light);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
.umds-list__item:has(> .umds-list-item--selected.umds-list-item--interactive):has(> .umds-list-item__actions):hover {
|
|
171
|
+
background: color-mix(
|
|
172
|
+
in srgb,
|
|
173
|
+
var(--umds-semantic-color-primary-light) 85%,
|
|
174
|
+
var(--umds-semantic-color-background-white)
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.umds-list-item__actions {
|
|
179
|
+
display: inline-flex;
|
|
180
|
+
flex-shrink: 0;
|
|
181
|
+
align-items: center;
|
|
182
|
+
gap: var(--umds-primitive-spacing-quarck); /* 4px */
|
|
183
|
+
}
|
|
@@ -27,6 +27,12 @@ export type ListItemTag = "button" | "a" | "div";
|
|
|
27
27
|
* HTML: é o título da linha. O atributo `title` (a dica nativa do navegador) é a
|
|
28
28
|
* prop `tooltip`.
|
|
29
29
|
*
|
|
30
|
+
* `actions` é o lugar de controles da linha (o ⋮ de um menu, um botão de
|
|
31
|
+
* excluir): fica no `li`, AO LADO do elemento clicável e não dentro dele, então
|
|
32
|
+
* o clique no controle não aciona a linha e o HTML continua válido. A linha toda
|
|
33
|
+
* (com as ações) ganha o fundo de hover e de seleção. Só existe com o `li`
|
|
34
|
+
* (sem `standalone`).
|
|
35
|
+
*
|
|
30
36
|
* `standalone` tira o `li`: o ListItem vira só o elemento interativo, para uso
|
|
31
37
|
* fora de um `List` (botão de número de página, item de carrossel, linha dentro
|
|
32
38
|
* de um `div`). Fora de `ul`/`ol`, o `li` é HTML inválido.
|
|
@@ -43,6 +49,7 @@ export type ListItemTag = "button" | "a" | "div";
|
|
|
43
49
|
* @property {any} meta - Terceira linha, menor (data, contexto): texto, ou slot `meta` (nó)
|
|
44
50
|
* @property {any} start - Slot `start`: conteúdo antes do texto (avatar, ícone)
|
|
45
51
|
* @property {any} end - Slot `end`: conteúdo no fim da linha (badge, seta, valor)
|
|
52
|
+
* @property {any} actions - Slot `actions`: controles da linha (menu ⋮, botão), fora do elemento clicável
|
|
46
53
|
* @property {function} onClick - Callback do clique (ou de Enter/Espaço no as="div")
|
|
47
54
|
* @property {string} className - Classes CSS adicionais, no elemento interativo
|
|
48
55
|
* @slot start - Conteúdo antes do texto.
|
|
@@ -50,6 +57,7 @@ export type ListItemTag = "button" | "a" | "div";
|
|
|
50
57
|
* @slot subtitle - Segunda linha (substitui a prop `subtitle`).
|
|
51
58
|
* @slot meta - Terceira linha (substitui a prop `meta`).
|
|
52
59
|
* @slot end - Conteúdo no fim da linha.
|
|
60
|
+
* @slot actions - Controles da linha, ao lado do elemento clicável (não dentro dele).
|
|
53
61
|
* @quandoUsar Itens de uma lista em que a linha toda é a ação: documentos não lidos, notificações, resultados de busca, opções de menu de filtro, itens de navegação secundária. Hover, foco visível e seleção já vêm resolvidos.
|
|
54
62
|
* @quandoNaoUsar Para dados comparados coluna a coluna, use `DataTable`. Para um cartão com título, ações e elevação própria, use `Card`. Para um botão solto fora de lista, use `Button`. Para a navegação principal do produto, use `SidePanelItem` ou `AppNavItem`.
|
|
55
63
|
*/
|
|
@@ -81,6 +89,8 @@ export interface ListItemProps {
|
|
|
81
89
|
start?: any;
|
|
82
90
|
/** Conteúdo no fim da linha (badge, seta, valor). No Vue, slot `end`. */
|
|
83
91
|
end?: any;
|
|
92
|
+
/** Controles da linha (menu ⋮, botão), fora do elemento clicável. Só com o `li`. No Vue, slot `actions`. */
|
|
93
|
+
actions?: any;
|
|
84
94
|
/** Chamado no clique da linha (ou em Enter/Espaço no `as="div"`). */
|
|
85
95
|
onClick?: (event: any) => void;
|
|
86
96
|
/** Classes CSS adicionais, no elemento interativo. */
|
|
@@ -77,6 +77,6 @@ function ListItem(props) {
|
|
|
77
77
|
}
|
|
78
78
|
return out;
|
|
79
79
|
}
|
|
80
|
-
return (
|
|
80
|
+
return (_jsx(_Fragment, { children: !props.standalone ? (_jsx(_Fragment, { children: _jsxs("li", { className: "umds-list__item", children: [elementTag() === "button" ? (_jsxs("button", { type: "button", id: props.id, title: props.tooltip, disabled: props.disabled, "aria-current": ariaCurrent(), onClick: (event) => handleClick(event), ...htmlAttrs(), className: classes(), children: [props.start ? (_jsx("span", { className: "umds-list-item__start", children: _jsx(_Fragment, { children: props.start }) })) : null, _jsxs("span", { className: "umds-list-item__body", children: [props.title ? (_jsx("span", { className: "umds-list-item__title", children: _jsx(Text, { variant: "body2", as: "span", weight: "semibold", color: "inherit", children: _jsx(_Fragment, { children: props.title }) }) })) : null, props.subtitle ? (_jsx("span", { className: "umds-list-item__subtitle", children: _jsx(Text, { variant: "body2", as: "span", color: "inherit", children: _jsx(_Fragment, { children: props.subtitle }) }) })) : null, props.meta ? (_jsx("span", { className: "umds-list-item__meta", children: _jsx(Text, { variant: "caption", as: "span", color: "inherit", children: _jsx(_Fragment, { children: props.meta }) }) })) : null, _jsx(_Fragment, { children: props.children })] }), props.end ? (_jsx("span", { className: "umds-list-item__end", children: _jsx(_Fragment, { children: props.end }) })) : null] })) : null, elementTag() === "a" ? (_jsxs("a", { id: props.id, title: props.tooltip, href: hrefValue(), target: props.target, rel: relValue(), "aria-current": ariaCurrent(), "aria-disabled": ariaDisabled(), onClick: (event) => handleClick(event), ...htmlAttrs(), className: classes(), children: [props.start ? (_jsx("span", { className: "umds-list-item__start", children: _jsx(_Fragment, { children: props.start }) })) : null, _jsxs("span", { className: "umds-list-item__body", children: [props.title ? (_jsx("span", { className: "umds-list-item__title", children: _jsx(Text, { variant: "body2", as: "span", weight: "semibold", color: "inherit", children: _jsx(_Fragment, { children: props.title }) }) })) : null, props.subtitle ? (_jsx("span", { className: "umds-list-item__subtitle", children: _jsx(Text, { variant: "body2", as: "span", color: "inherit", children: _jsx(_Fragment, { children: props.subtitle }) }) })) : null, props.meta ? (_jsx("span", { className: "umds-list-item__meta", children: _jsx(Text, { variant: "caption", as: "span", color: "inherit", children: _jsx(_Fragment, { children: props.meta }) }) })) : null, _jsx(_Fragment, { children: props.children })] }), props.end ? (_jsx("span", { className: "umds-list-item__end", children: _jsx(_Fragment, { children: props.end }) })) : null] })) : null, elementTag() === "div" ? (_jsxs("div", { id: props.id, title: props.tooltip, role: divRole(), tabIndex: divTabIndex(), "aria-current": ariaCurrent(), "aria-disabled": ariaDisabled(), onClick: (event) => handleClick(event), onKeyDown: (event) => handleKeyDown(event), ...htmlAttrs(), className: classes(), children: [props.start ? (_jsx("span", { className: "umds-list-item__start", children: _jsx(_Fragment, { children: props.start }) })) : null, _jsxs("span", { className: "umds-list-item__body", children: [props.title ? (_jsx("span", { className: "umds-list-item__title", children: _jsx(Text, { variant: "body2", as: "span", weight: "semibold", color: "inherit", children: _jsx(_Fragment, { children: props.title }) }) })) : null, props.subtitle ? (_jsx("span", { className: "umds-list-item__subtitle", children: _jsx(Text, { variant: "body2", as: "span", color: "inherit", children: _jsx(_Fragment, { children: props.subtitle }) }) })) : null, props.meta ? (_jsx("span", { className: "umds-list-item__meta", children: _jsx(Text, { variant: "caption", as: "span", color: "inherit", children: _jsx(_Fragment, { children: props.meta }) }) })) : null, _jsx(_Fragment, { children: props.children })] }), props.end ? (_jsx("span", { className: "umds-list-item__end", children: _jsx(_Fragment, { children: props.end }) })) : null] })) : null, props.actions ? (_jsx("div", { className: "umds-list-item__actions", children: _jsx(_Fragment, { children: props.actions }) })) : null] }) })) : (_jsx(_Fragment, { children: _jsxs(_Fragment, { children: [elementTag() === "button" ? (_jsxs("button", { type: "button", id: props.id, title: props.tooltip, disabled: props.disabled, "aria-current": ariaCurrent(), onClick: (event) => handleClick(event), ...htmlAttrs(), className: classes(), children: [props.start ? (_jsx("span", { className: "umds-list-item__start", children: _jsx(_Fragment, { children: props.start }) })) : null, _jsxs("span", { className: "umds-list-item__body", children: [props.title ? (_jsx("span", { className: "umds-list-item__title", children: _jsx(Text, { variant: "body2", as: "span", weight: "semibold", color: "inherit", children: _jsx(_Fragment, { children: props.title }) }) })) : null, props.subtitle ? (_jsx("span", { className: "umds-list-item__subtitle", children: _jsx(Text, { variant: "body2", as: "span", color: "inherit", children: _jsx(_Fragment, { children: props.subtitle }) }) })) : null, props.meta ? (_jsx("span", { className: "umds-list-item__meta", children: _jsx(Text, { variant: "caption", as: "span", color: "inherit", children: _jsx(_Fragment, { children: props.meta }) }) })) : null, _jsx(_Fragment, { children: props.children })] }), props.end ? (_jsx("span", { className: "umds-list-item__end", children: _jsx(_Fragment, { children: props.end }) })) : null] })) : null, elementTag() === "a" ? (_jsxs("a", { id: props.id, title: props.tooltip, href: hrefValue(), target: props.target, rel: relValue(), "aria-current": ariaCurrent(), "aria-disabled": ariaDisabled(), onClick: (event) => handleClick(event), ...htmlAttrs(), className: classes(), children: [props.start ? (_jsx("span", { className: "umds-list-item__start", children: _jsx(_Fragment, { children: props.start }) })) : null, _jsxs("span", { className: "umds-list-item__body", children: [props.title ? (_jsx("span", { className: "umds-list-item__title", children: _jsx(Text, { variant: "body2", as: "span", weight: "semibold", color: "inherit", children: _jsx(_Fragment, { children: props.title }) }) })) : null, props.subtitle ? (_jsx("span", { className: "umds-list-item__subtitle", children: _jsx(Text, { variant: "body2", as: "span", color: "inherit", children: _jsx(_Fragment, { children: props.subtitle }) }) })) : null, props.meta ? (_jsx("span", { className: "umds-list-item__meta", children: _jsx(Text, { variant: "caption", as: "span", color: "inherit", children: _jsx(_Fragment, { children: props.meta }) }) })) : null, _jsx(_Fragment, { children: props.children })] }), props.end ? (_jsx("span", { className: "umds-list-item__end", children: _jsx(_Fragment, { children: props.end }) })) : null] })) : null, elementTag() === "div" ? (_jsxs("div", { id: props.id, title: props.tooltip, role: divRole(), tabIndex: divTabIndex(), "aria-current": ariaCurrent(), "aria-disabled": ariaDisabled(), onClick: (event) => handleClick(event), onKeyDown: (event) => handleKeyDown(event), ...htmlAttrs(), className: classes(), children: [props.start ? (_jsx("span", { className: "umds-list-item__start", children: _jsx(_Fragment, { children: props.start }) })) : null, _jsxs("span", { className: "umds-list-item__body", children: [props.title ? (_jsx("span", { className: "umds-list-item__title", children: _jsx(Text, { variant: "body2", as: "span", weight: "semibold", color: "inherit", children: _jsx(_Fragment, { children: props.title }) }) })) : null, props.subtitle ? (_jsx("span", { className: "umds-list-item__subtitle", children: _jsx(Text, { variant: "body2", as: "span", color: "inherit", children: _jsx(_Fragment, { children: props.subtitle }) }) })) : null, props.meta ? (_jsx("span", { className: "umds-list-item__meta", children: _jsx(Text, { variant: "caption", as: "span", color: "inherit", children: _jsx(_Fragment, { children: props.meta }) }) })) : null, _jsx(_Fragment, { children: props.children })] }), props.end ? (_jsx("span", { className: "umds-list-item__end", children: _jsx(_Fragment, { children: props.end }) })) : null] })) : null] }) })) }));
|
|
81
81
|
}
|
|
82
82
|
export default ListItem;
|
|
@@ -243,8 +243,25 @@
|
|
|
243
243
|
color: var(--umds-semantic-color-feedback-info-dark);
|
|
244
244
|
}
|
|
245
245
|
|
|
246
|
+
/* ── secondaryTone: cor da segunda medida (mesmas variantes do valueTone) ── */
|
|
247
|
+
.umds-stat-card--secundario-primary .umds-stat-card__valor-secundario {
|
|
248
|
+
color: var(--umds-semantic-color-primary-pure);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.umds-stat-card--secundario-positive .umds-stat-card__valor-secundario {
|
|
252
|
+
color: var(--umds-semantic-color-feedback-positive-dark);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
.umds-stat-card--secundario-negative .umds-stat-card__valor-secundario {
|
|
256
|
+
color: var(--umds-semantic-color-feedback-negative-dark);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.umds-stat-card--secundario-info .umds-stat-card__valor-secundario {
|
|
260
|
+
color: var(--umds-semantic-color-feedback-info-dark);
|
|
261
|
+
}
|
|
262
|
+
|
|
246
263
|
/* ── Segunda medida (secondaryValue, secondaryLabel) ──
|
|
247
|
-
|
|
264
|
+
À direita da principal, separada por um filete. Vem depois do corpo no HTML. */
|
|
248
265
|
.umds-stat-card__secundaria {
|
|
249
266
|
display: flex;
|
|
250
267
|
flex-direction: column;
|
|
@@ -263,8 +280,9 @@
|
|
|
263
280
|
text-align: center;
|
|
264
281
|
}
|
|
265
282
|
|
|
283
|
+
/* Mesmo tamanho e peso do número principal: as duas medidas se comparam lado a lado. */
|
|
266
284
|
.umds-stat-card__valor-secundario {
|
|
267
|
-
font-size: var(--umds-primitive-font-size-
|
|
285
|
+
font-size: var(--umds-primitive-font-size-md);
|
|
268
286
|
font-weight: var(--umds-primitive-font-weight-bold);
|
|
269
287
|
line-height: 1.15;
|
|
270
288
|
font-variant-numeric: tabular-nums;
|
|
@@ -285,6 +303,30 @@
|
|
|
285
303
|
.umds-stat-card--rotulo-topo .umds-stat-card__secundaria {
|
|
286
304
|
grid-column: 3;
|
|
287
305
|
grid-row: 2;
|
|
306
|
+
/* A grade não tem vão entre colunas: o filete ganha o mesmo respiro dos dois lados. */
|
|
307
|
+
margin-left: var(--umds-primitive-spacing-xxxs);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.umds-stat-card--rotulo-topo .umds-stat-card__valor-secundario {
|
|
311
|
+
font-size: var(--umds-primitive-font-size-lg); /* 32px, como o principal */
|
|
312
|
+
font-weight: var(--umds-primitive-font-weight-extrabold);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/* Com rótulo na segunda medida, ela ocupa as linhas do número e da descrição
|
|
316
|
+
(subgrid): o rótulo dela fica na linha da descrição ("Geral" ao lado de
|
|
317
|
+
"Meu Setor") e o valor na linha do número principal. */
|
|
318
|
+
.umds-stat-card--rotulo-topo .umds-stat-card__secundaria:has(.umds-stat-card__rotulo-secundario) {
|
|
319
|
+
grid-row: 2 / span 2;
|
|
320
|
+
display: grid;
|
|
321
|
+
grid-template-rows: subgrid;
|
|
322
|
+
/* `normal` usa o vão da grade do cartão; o `micro` da regra base deslocaria a linha em 1,5px. */
|
|
323
|
+
row-gap: normal;
|
|
324
|
+
align-items: center;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.umds-stat-card--rotulo-topo.umds-stat-card--com-secundaria .umds-stat-card__descricao {
|
|
328
|
+
grid-column: 1 / 3;
|
|
329
|
+
grid-row: 3;
|
|
288
330
|
}
|
|
289
331
|
|
|
290
332
|
.umds-stat-card--rotulo-topo.umds-stat-card--com-secundaria:not(:has(.umds-stat-card__icone)) .umds-stat-card__valor {
|
|
@@ -298,3 +340,16 @@
|
|
|
298
340
|
font-size: 1.125rem; /* 18px */
|
|
299
341
|
font-weight: var(--umds-primitive-font-weight-extrabold);
|
|
300
342
|
}
|
|
343
|
+
|
|
344
|
+
/* A segunda medida acompanha o número principal. */
|
|
345
|
+
.umds-stat-card.umds-stat-card--sm .umds-stat-card__valor-secundario {
|
|
346
|
+
font-size: 1.125rem; /* 18px */
|
|
347
|
+
font-weight: var(--umds-primitive-font-weight-extrabold);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/* ── loading: o Skeleton ocupa a altura do número (1em da fonte dele) ── */
|
|
351
|
+
.umds-stat-card[aria-busy="true"] .umds-stat-card__valor,
|
|
352
|
+
.umds-stat-card[aria-busy="true"] .umds-stat-card__valor-secundario {
|
|
353
|
+
display: inline-flex;
|
|
354
|
+
align-items: center;
|
|
355
|
+
}
|
|
@@ -14,7 +14,7 @@ import './StatCard.css';
|
|
|
14
14
|
* pessoa a segurar o rótulo na cabeça esperando o valor.
|
|
15
15
|
* - **A variação não depende só da cor.** Sobe e desce vêm com seta, porque
|
|
16
16
|
* verde e vermelho sozinhos não comunicam para quem não distingue cores.
|
|
17
|
-
* @property {
|
|
17
|
+
* @property {any} value - O número ou medida em destaque. Aceita um nó, como um `Counter` para o número contar até o valor (no Vue, slot `value`)
|
|
18
18
|
* @property {string} label - O que o número significa
|
|
19
19
|
* @property {IconProp} icon - Ícone Font Awesome como objeto importado
|
|
20
20
|
* @property {string} variant - Cor de apoio do ícone e da variação
|
|
@@ -24,20 +24,22 @@ import './StatCard.css';
|
|
|
24
24
|
* @property {string} description - Texto auxiliar abaixo do rótulo
|
|
25
25
|
* @property {string} size - 'md' (padrão) | 'sm': o `sm` leva o número a 18px/800, o tamanho dos KPIs da home do CrisDoc (fora da escala de fonte do DS, que vai de 16 a 20). Vale também com `labelPosition="top"`.
|
|
26
26
|
* @property {string} labelPosition - Onde o rótulo aparece: 'bottom' (padrão, embaixo do número) | 'top' (no topo do cartão, acima do ícone e do número, com o número em 32/800). No HTML o número continua vindo antes do rótulo, então o leitor de tela anuncia igual.
|
|
27
|
-
* @property {string|number} secondaryValue - Segunda medida do cartão,
|
|
27
|
+
* @property {string|number} secondaryValue - Segunda medida do cartão, no mesmo tamanho da principal, à direita dela ("22,31%" do setor e "14,11%" do geral). Com `labelPosition="top"`, o `secondaryLabel` fica na linha da `description`. Vem depois do resto no HTML. `0` aparece; vazio e ausente não.
|
|
28
28
|
* @property {string} secondaryLabel - O que a segunda medida significa ("Geral")
|
|
29
29
|
* @property {string} valueTone - Cor do número principal: 'default' (cor do texto) | 'primary' | 'positive' | 'negative' | 'info'. Usa as variantes escuras (contraste AA sobre o branco). Não confunda com `variant`, que pinta o ícone e a variação.
|
|
30
|
+
* @property {string} secondaryTone - Cor da segunda medida, com as mesmas opções de `valueTone` ("Meu Setor" em azul e "Geral" em verde, por exemplo).
|
|
31
|
+
* @property {boolean} loading - Carregando: os números viram Skeleton do tamanho deles (rótulos e ⓘ continuam), a variação some e o cartão ganha `aria-busy`. Dispensa montar o Skeleton por fora.
|
|
30
32
|
* @property {string} infoTooltip - Texto de ajuda: mostra um ⓘ ao lado do rótulo com um Tooltip escuro. É um botão dentro do cartão; com `onClick` no cartão, evite (botão dentro de botão).
|
|
31
33
|
* @property {string} infoLabel - Nome acessível do ⓘ. Troque para traduzir. Padrão: "Mais informações".
|
|
32
34
|
* @property {string} align - Alinhamento do conteúdo
|
|
33
35
|
* @property {string} className - Classes CSS adicionais
|
|
34
36
|
* @property {function} onClick - Torna o cartão acionável (vira botão)
|
|
35
37
|
* @quandoUsar Painel e visão geral: destacar UMA medida por cartão, em linha com outras. O número é o protagonista da leitura.
|
|
36
|
-
* @quandoNaoUsar Para listar dados comparáveis — use `DataTable`. Para um bloco com título e conteúdo livre, `Card`. Para um rótulo curto de estado, `Tag` ou `Badge`. Não empilhe vários números num StatCard só:
|
|
38
|
+
* @quandoNaoUsar Para listar dados comparáveis — use `DataTable`. Para um bloco com título e conteúdo livre, `Card`. Para um rótulo curto de estado, `Tag` ou `Badge`. Não empilhe vários números num StatCard só: uma medida, ou duas comparadas (`secondaryValue`).
|
|
37
39
|
*/
|
|
38
40
|
export interface StatCardProps {
|
|
39
|
-
/** O número ou medida em destaque. */
|
|
40
|
-
value?:
|
|
41
|
+
/** O número ou medida em destaque. Texto, número ou nó (um `Counter`); no Vue, slot `value`. */
|
|
42
|
+
value?: any;
|
|
41
43
|
/** O que o número significa. */
|
|
42
44
|
label?: string;
|
|
43
45
|
/** Ícone Font Awesome como objeto importado. */
|
|
@@ -70,7 +72,7 @@ export interface StatCardProps {
|
|
|
70
72
|
* @default 'bottom'
|
|
71
73
|
*/
|
|
72
74
|
labelPosition?: "bottom" | "top";
|
|
73
|
-
/** Segunda medida,
|
|
75
|
+
/** Segunda medida, no tamanho da principal, à direita dela. 0 aparece; vazio e ausente não. */
|
|
74
76
|
secondaryValue?: string | number;
|
|
75
77
|
/** O que a segunda medida significa. */
|
|
76
78
|
secondaryLabel?: string;
|
|
@@ -78,6 +80,10 @@ export interface StatCardProps {
|
|
|
78
80
|
size?: "md" | "sm";
|
|
79
81
|
/** Cor do número principal. @default 'default' */
|
|
80
82
|
valueTone?: "default" | "primary" | "positive" | "negative" | "info";
|
|
83
|
+
/** Cor da segunda medida. Mesmas opções de `valueTone`. @default 'default' */
|
|
84
|
+
secondaryTone?: "default" | "primary" | "positive" | "negative" | "info";
|
|
85
|
+
/** Carregando: números viram Skeleton, a variação some, `aria-busy` no cartão. @default false */
|
|
86
|
+
loading?: boolean;
|
|
81
87
|
/**
|
|
82
88
|
* Texto de ajuda: mostra o ⓘ ao lado do rótulo, com um Tooltip escuro. É um
|
|
83
89
|
* botão focável dentro do cartão; com `onClick` no cartão, evite.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
"use client";
|
|
2
|
-
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import './StatCard.css';
|
|
4
4
|
import { faCircleInfo } from "@fortawesome/free-solid-svg-icons";
|
|
5
5
|
import Icon from "../Icon/Icon";
|
|
6
6
|
import Tooltip from "../Tooltip/Tooltip";
|
|
7
|
+
import Skeleton from "../Skeleton/Skeleton";
|
|
7
8
|
function StatCard(props) {
|
|
8
9
|
function classes() {
|
|
9
10
|
return [
|
|
@@ -16,6 +17,9 @@ function StatCard(props) {
|
|
|
16
17
|
props.valueTone && props.valueTone !== "default"
|
|
17
18
|
? "umds-stat-card--valor-" + props.valueTone
|
|
18
19
|
: "",
|
|
20
|
+
props.secondaryTone && props.secondaryTone !== "default"
|
|
21
|
+
? "umds-stat-card--secundario-" + props.secondaryTone
|
|
22
|
+
: "",
|
|
19
23
|
props.onClick ? "umds-stat-card--acionavel" : "",
|
|
20
24
|
props.className,
|
|
21
25
|
]
|
|
@@ -58,6 +62,6 @@ function StatCard(props) {
|
|
|
58
62
|
if (props.onClick)
|
|
59
63
|
props.onClick();
|
|
60
64
|
}
|
|
61
|
-
return (_jsxs("div", { className: classes(), onClick: (event) => aoClicar(), role: ehAcionavel() ? "button" : undefined, tabIndex: ehAcionavel() ? 0 : undefined, children: [props.icon ? (_jsx("span", { className: "umds-stat-card__icone", "aria-hidden": "true", children: _jsx(Icon, { size: "md", variant: "inherit", icon: props.icon }) })) : null, _jsxs("div", { className: "umds-stat-card__corpo", children: [
|
|
65
|
+
return (_jsxs("div", { className: classes(), onClick: (event) => aoClicar(), role: ehAcionavel() ? "button" : undefined, tabIndex: ehAcionavel() ? 0 : undefined, "aria-busy": props.loading ? "true" : undefined, children: [props.icon ? (_jsx("span", { className: "umds-stat-card__icone", "aria-hidden": "true", children: _jsx(Icon, { size: "md", variant: "inherit", icon: props.icon }) })) : null, _jsxs("div", { className: "umds-stat-card__corpo", children: [_jsxs("span", { className: "umds-stat-card__valor", children: [props.loading ? _jsx(Skeleton, { width: "3em", height: "1em" }) : null, !props.loading ? _jsx(_Fragment, { children: props.value }) : null] }), _jsxs("span", { className: "umds-stat-card__rotulo", children: [props.label, props.infoTooltip ? (_jsx(Tooltip, { variant: "dark", side: "top", content: props.infoTooltip, children: _jsx("button", { type: "button", className: "umds-stat-card__info", "aria-label": props.infoLabel || "Mais informações", children: _jsx(Icon, { variant: "inherit", customSize: "14px", icon: faCircleInfo }) }) })) : null] }), props.description ? (_jsx("span", { className: "umds-stat-card__descricao", children: props.description })) : null, props.trendLabel && !props.loading ? (_jsxs("span", { className: classesDaVariacao(), children: [_jsx("span", { "aria-hidden": "true", children: simboloDaVariacao() }), props.trendLabel] })) : null] }), temSecundaria() ? (_jsxs("div", { className: "umds-stat-card__secundaria", children: [_jsxs("span", { className: "umds-stat-card__valor-secundario", children: [props.loading ? _jsx(Skeleton, { width: "3em", height: "1em" }) : null, !props.loading ? _jsx(_Fragment, { children: props.secondaryValue }) : null] }), props.secondaryLabel ? (_jsx("span", { className: "umds-stat-card__rotulo-secundario", children: props.secondaryLabel })) : null] })) : null] }));
|
|
62
66
|
}
|
|
63
67
|
export default StatCard;
|
|
@@ -8,6 +8,9 @@
|
|
|
8
8
|
───────────────────────────────────────────── */
|
|
9
9
|
|
|
10
10
|
.umds-tooltip {
|
|
11
|
+
/* Bloco de contenção da cópia escondida (__descricao, absolute): sem ele,
|
|
12
|
+
ela escapa do overflow de uma área que rola e estica a página. */
|
|
13
|
+
position: relative;
|
|
11
14
|
display: inline-flex;
|
|
12
15
|
height: fit-content;
|
|
13
16
|
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
3
3
|
import './Tooltip.css';
|
|
4
4
|
import { useState, useRef, useEffect } from "react";
|
|
5
|
-
import { computePosition, autoUpdate, offset, flip, shift, } from "@floating-ui/dom";
|
|
5
|
+
import { computePosition, autoUpdate, offset, flip, shift, arrow, } from "@floating-ui/dom";
|
|
6
6
|
import Icon from "../Icon/Icon";
|
|
7
7
|
import Portal from "../Portal/Portal";
|
|
8
8
|
import { gerarId } from "../../utils/id";
|
|
@@ -28,6 +28,7 @@ function Tooltip(props) {
|
|
|
28
28
|
const [posY, setPosY] = useState(() => 0);
|
|
29
29
|
const [finalSide, setFinalSide] = useState(() => "top");
|
|
30
30
|
const [finalAlign, setFinalAlign] = useState(() => "center");
|
|
31
|
+
const [arrowOffset, setArrowOffset] = useState(() => -1);
|
|
31
32
|
const [bubbleId, setBubbleId] = useState(() => "");
|
|
32
33
|
const [descId, setDescId] = useState(() => "");
|
|
33
34
|
function bubbleIdText() {
|
|
@@ -92,6 +93,12 @@ function Tooltip(props) {
|
|
|
92
93
|
padding: 8,
|
|
93
94
|
}),
|
|
94
95
|
];
|
|
96
|
+
// A seta acompanha o gatilho quando o balão é empurrado para caber.
|
|
97
|
+
if (arrowRef.current)
|
|
98
|
+
middlewares.push(arrow({
|
|
99
|
+
element: arrowRef.current,
|
|
100
|
+
padding: 8,
|
|
101
|
+
}));
|
|
95
102
|
computePosition(triggerRef.current, floatingRef.current, {
|
|
96
103
|
placement,
|
|
97
104
|
strategy: "fixed",
|
|
@@ -105,6 +112,14 @@ function Tooltip(props) {
|
|
|
105
112
|
const parts = position.placement.split("-");
|
|
106
113
|
setFinalSide(parts[0]);
|
|
107
114
|
setFinalAlign(parts[1] || "center");
|
|
115
|
+
// Só quando o shift deslocou o balão a seta sai da posição estática
|
|
116
|
+
// (as 12 classes side×align): nos outros casos o visual não muda.
|
|
117
|
+
const deslocamento = position.middlewareData.shift;
|
|
118
|
+
const seta = position.middlewareData.arrow;
|
|
119
|
+
const empurrado = !!deslocamento && (deslocamento.x !== 0 || deslocamento.y !== 0);
|
|
120
|
+
const vertical = parts[0] === "left" || parts[0] === "right";
|
|
121
|
+
const eixo = seta ? (vertical ? seta.y : seta.x) : undefined;
|
|
122
|
+
setArrowOffset(empurrado && typeof eixo === "number" ? eixo : -1);
|
|
108
123
|
setPositioned(true);
|
|
109
124
|
});
|
|
110
125
|
}
|
|
@@ -273,6 +288,22 @@ function Tooltip(props) {
|
|
|
273
288
|
"umds-tooltip__arrow--align-" + finalAlign,
|
|
274
289
|
].join(" ");
|
|
275
290
|
}
|
|
291
|
+
function arrowStyle() {
|
|
292
|
+
if (arrowOffset < 0)
|
|
293
|
+
return undefined;
|
|
294
|
+
if (finalSide === "left" || finalSide === "right") {
|
|
295
|
+
return {
|
|
296
|
+
top: arrowOffset + "px",
|
|
297
|
+
bottom: "auto",
|
|
298
|
+
transform: "rotate(45deg)",
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
return {
|
|
302
|
+
left: arrowOffset + "px",
|
|
303
|
+
right: "auto",
|
|
304
|
+
transform: "rotate(45deg)",
|
|
305
|
+
};
|
|
306
|
+
}
|
|
276
307
|
useEffect(() => {
|
|
277
308
|
if (!triggerRef.current)
|
|
278
309
|
return;
|
|
@@ -315,6 +346,6 @@ function Tooltip(props) {
|
|
|
315
346
|
stopKeydownWatch();
|
|
316
347
|
};
|
|
317
348
|
}, []);
|
|
318
|
-
return (_jsxs("span", { ref: triggerRef, className: ["umds-tooltip", props.className].filter(Boolean).join(" "), onMouseEnter: (event) => handleOpen(), onMouseLeave: (event) => handleClose(event), children: [_jsx(_Fragment, { children: props.children }), _jsxs("span", { className: "umds-tooltip__descricao", "aria-hidden": "true", id: descIdText(), children: [props.title ? _jsx(_Fragment, { children: tituloDescrito() }) : null, props.content ? _jsx(_Fragment, { children: props.content }) : null] }), _jsx(Portal, { children: mounted ? (_jsx("span", { ref: floatingRef, className: floatingClasses(), style: floatingStyle(), children: _jsxs("span", { role: "tooltip", className: bubbleClasses(), id: bubbleIdText(), "aria-hidden": !open, children: [_jsxs("span", { className: "umds-tooltip__row", children: [props.icon ? (_jsx(Icon, { size: "sm", variant: "inherit", icon: props.icon })) : null, _jsxs("span", { className: "umds-tooltip__text", children: [props.title ? (_jsx("span", { className: "umds-tooltip__title", children: props.title })) : null, props.content ? (_jsx("span", { className: "umds-tooltip__content", children: _jsx(_Fragment, { children: props.content }) })) : null] })] }), _jsx("span", { ref: arrowRef, className: arrowClasses() })] }) })) : null })] }));
|
|
349
|
+
return (_jsxs("span", { ref: triggerRef, className: ["umds-tooltip", props.className].filter(Boolean).join(" "), onMouseEnter: (event) => handleOpen(), onMouseLeave: (event) => handleClose(event), children: [_jsx(_Fragment, { children: props.children }), _jsxs("span", { className: "umds-tooltip__descricao", "aria-hidden": "true", id: descIdText(), children: [props.title ? _jsx(_Fragment, { children: tituloDescrito() }) : null, props.content ? _jsx(_Fragment, { children: props.content }) : null] }), _jsx(Portal, { children: mounted ? (_jsx("span", { ref: floatingRef, className: floatingClasses(), style: floatingStyle(), children: _jsxs("span", { role: "tooltip", className: bubbleClasses(), id: bubbleIdText(), "aria-hidden": !open, children: [_jsxs("span", { className: "umds-tooltip__row", children: [props.icon ? (_jsx(Icon, { size: "sm", variant: "inherit", icon: props.icon })) : null, _jsxs("span", { className: "umds-tooltip__text", children: [props.title ? (_jsx("span", { className: "umds-tooltip__title", children: props.title })) : null, props.content ? (_jsx("span", { className: "umds-tooltip__content", children: _jsx(_Fragment, { children: props.content }) })) : null] })] }), _jsx("span", { ref: arrowRef, className: arrowClasses(), style: arrowStyle() })] }) })) : null })] }));
|
|
319
350
|
}
|
|
320
351
|
export default Tooltip;
|
package/dist/index.d.ts
CHANGED
|
@@ -50,6 +50,8 @@ export { default as Input } from "./components/Input/Input";
|
|
|
50
50
|
export { default as Kanban } from "./components/Kanban/Kanban";
|
|
51
51
|
export { default as KanbanCard } from "./components/Kanban/KanbanCard";
|
|
52
52
|
export { default as KanbanColumn } from "./components/Kanban/KanbanColumn";
|
|
53
|
+
export { default as Counter } from "./components/Counter/Counter";
|
|
54
|
+
export type * from "./components/Counter/Counter";
|
|
53
55
|
export { default as Kbd } from "./components/Kbd/Kbd";
|
|
54
56
|
export { default as Link } from "./components/Link/Link";
|
|
55
57
|
export { default as LineChart } from "./components/Chart/LineChart";
|
package/dist/index.js
CHANGED
|
@@ -56,6 +56,7 @@ export { default as Input } from "./components/Input/Input";
|
|
|
56
56
|
export { default as Kanban } from "./components/Kanban/Kanban";
|
|
57
57
|
export { default as KanbanCard } from "./components/Kanban/KanbanCard";
|
|
58
58
|
export { default as KanbanColumn } from "./components/Kanban/KanbanColumn";
|
|
59
|
+
export { default as Counter } from "./components/Counter/Counter";
|
|
59
60
|
export { default as Kbd } from "./components/Kbd/Kbd";
|
|
60
61
|
export { default as Link } from "./components/Link/Link";
|
|
61
62
|
export { default as LineChart } from "./components/Chart/LineChart";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { describe, expect, it, vi } from "vitest";
|
|
3
|
+
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
4
|
+
import Button from "../components/Button/Button";
|
|
5
|
+
import Counter from "../components/Counter/Counter";
|
|
6
|
+
import Dropdown from "../components/Dropdown/Dropdown";
|
|
7
|
+
import ListItem from "../components/List/ListItem";
|
|
8
|
+
import StatCard from "../components/StatCard/StatCard";
|
|
9
|
+
describe("Counter", () => {
|
|
10
|
+
it("sem animação mostra o valor final formatado em pt-BR", () => {
|
|
11
|
+
const { container } = render(_jsx(Counter, { value: 1284, disableAnimation: true }));
|
|
12
|
+
expect(container.querySelector(".umds-counter__numero")).toHaveTextContent("1.284");
|
|
13
|
+
expect(container.querySelector(".umds-counter__numero")).toHaveAttribute("aria-hidden", "true");
|
|
14
|
+
expect(container.querySelector(".umds-counter__leitura")).toHaveTextContent("1.284");
|
|
15
|
+
});
|
|
16
|
+
it("casas decimais seguem o value; prefixo e sufixo entram no texto", () => {
|
|
17
|
+
const { container } = render(_jsxs(_Fragment, { children: [_jsx(Counter, { value: 22.31, suffix: "%", disableAnimation: true }), _jsx(Counter, { value: 15890.5, prefix: "R$ ", decimals: 2, disableAnimation: true })] }));
|
|
18
|
+
const leituras = container.querySelectorAll(".umds-counter__leitura");
|
|
19
|
+
expect(leituras[0]).toHaveTextContent("22,31%");
|
|
20
|
+
expect(leituras[1].textContent).toBe("R$ 15.890,50");
|
|
21
|
+
});
|
|
22
|
+
it("conta do from até o valor e termina no valor exato", async () => {
|
|
23
|
+
vi.useFakeTimers();
|
|
24
|
+
const { container } = render(_jsx(Counter, { value: 500, duration: 100 }));
|
|
25
|
+
const numero = () => container.querySelector(".umds-counter__numero").textContent;
|
|
26
|
+
expect(numero()).toBe("0");
|
|
27
|
+
await act(async () => {
|
|
28
|
+
vi.advanceTimersByTime(500);
|
|
29
|
+
});
|
|
30
|
+
expect(numero()).toBe("500");
|
|
31
|
+
// A reserva e a leitura já nascem com o valor final.
|
|
32
|
+
expect(container.querySelector(".umds-counter__leitura")).toHaveTextContent("500");
|
|
33
|
+
vi.useRealTimers();
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
describe("ListItem · actions", () => {
|
|
37
|
+
it("as ações ficam no li, fora do elemento clicável", () => {
|
|
38
|
+
const { container } = render(_jsx("ul", { children: _jsx(ListItem, { href: "#doc", title: "Memorando", actions: _jsx("button", { type: "button", children: "Mais" }) }) }));
|
|
39
|
+
const acao = screen.getByRole("button", { name: "Mais" });
|
|
40
|
+
expect(acao.closest("a")).toBeNull();
|
|
41
|
+
expect(acao.parentElement).toHaveClass("umds-list-item__actions");
|
|
42
|
+
expect(container.querySelector("li > a")).toHaveAttribute("href", "#doc");
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
describe("Button · href", () => {
|
|
46
|
+
it("com href é um link; desabilitado volta a ser botão", () => {
|
|
47
|
+
render(_jsxs(_Fragment, { children: [_jsx(Button, { href: "#inbox", children: "Inbox" }), _jsx(Button, { href: "#cal", target: "_blank", children: "Calend\u00E1rio" }), _jsx(Button, { href: "#fila", disabled: true, children: "Fila" })] }));
|
|
48
|
+
expect(screen.getByRole("link", { name: "Inbox" })).toHaveAttribute("href", "#inbox");
|
|
49
|
+
expect(screen.getByRole("link", { name: "Calendário" })).toHaveAttribute("rel", "noopener noreferrer");
|
|
50
|
+
expect(screen.getByRole("button", { name: "Fila" })).toBeDisabled();
|
|
51
|
+
});
|
|
52
|
+
it("loading segura a navegação do link", () => {
|
|
53
|
+
render(_jsx(Button, { href: "#inbox", loading: true, loadingPosition: "start", children: "Inbox" }));
|
|
54
|
+
const link = screen.getByRole("link", { name: "Inbox" });
|
|
55
|
+
const evento = new MouseEvent("click", { bubbles: true, cancelable: true });
|
|
56
|
+
link.dispatchEvent(evento);
|
|
57
|
+
expect(evento.defaultPrevented).toBe(true);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
describe("Dropdown · href", () => {
|
|
61
|
+
it("item com href vira <a role=menuitem>, e o onSelect continua", async () => {
|
|
62
|
+
const onSelect = vi.fn();
|
|
63
|
+
render(_jsx(Dropdown, { ariaLabel: "A\u00E7\u00F5es", onSelect: onSelect, items: [
|
|
64
|
+
{ key: "assinar", label: "Assinar", href: "#assinar" },
|
|
65
|
+
{ key: "arquivar", label: "Arquivar" },
|
|
66
|
+
], children: _jsx("button", { type: "button", children: "Abrir" }) }));
|
|
67
|
+
fireEvent.click(screen.getByRole("button", { name: "Abrir" }));
|
|
68
|
+
const assinar = await screen.findByRole("menuitem", { name: "Assinar" });
|
|
69
|
+
expect(assinar.tagName).toBe("A");
|
|
70
|
+
expect(assinar).toHaveAttribute("href", "#assinar");
|
|
71
|
+
expect(screen.getByRole("menuitem", { name: "Arquivar" }).tagName).toBe("BUTTON");
|
|
72
|
+
fireEvent.click(assinar);
|
|
73
|
+
expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ key: "assinar" }));
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
describe("StatCard · loading e secondaryTone", () => {
|
|
77
|
+
it("loading troca os números por Skeleton e marca aria-busy", () => {
|
|
78
|
+
const { container } = render(_jsx(StatCard, { value: "22,31%", label: "Qualidade", secondaryValue: "14,11%", trendLabel: "+2%", trend: "up", loading: true }));
|
|
79
|
+
const cartao = container.querySelector(".umds-stat-card");
|
|
80
|
+
expect(cartao).toHaveAttribute("aria-busy", "true");
|
|
81
|
+
expect(cartao.textContent).not.toContain("22,31%");
|
|
82
|
+
expect(cartao.textContent).not.toContain("14,11%");
|
|
83
|
+
expect(cartao.textContent).not.toContain("+2%");
|
|
84
|
+
expect(cartao.textContent).toContain("Qualidade");
|
|
85
|
+
});
|
|
86
|
+
it("secondaryTone escreve a classe; default não", () => {
|
|
87
|
+
const { container } = render(_jsxs(_Fragment, { children: [_jsx(StatCard, { value: "1", secondaryValue: "2", secondaryTone: "positive" }), _jsx(StatCard, { value: "1", secondaryValue: "2", secondaryTone: "default" })] }));
|
|
88
|
+
expect(container.children[0]).toHaveClass("umds-stat-card--secundario-positive");
|
|
89
|
+
expect(container.children[1].className).not.toContain("secundario-");
|
|
90
|
+
});
|
|
91
|
+
});
|
|
@@ -23,6 +23,14 @@ describe("StatCard · size", () => {
|
|
|
23
23
|
const { container } = render(_jsx(StatCard, { value: 44, label: "N\u00E3o lidos", size: "sm", labelPosition: "top" }));
|
|
24
24
|
expect(container.firstElementChild).toHaveClass("umds-stat-card--sm", "umds-stat-card--rotulo-topo");
|
|
25
25
|
});
|
|
26
|
+
it("a segunda medida tem o mesmo tamanho e peso do número, no sm e no rótulo no topo", () => {
|
|
27
|
+
const sm = bloco(".umds-stat-card.umds-stat-card--sm .umds-stat-card__valor-secundario");
|
|
28
|
+
expect(sm).toMatch(/font-size:\s*1\.125rem/);
|
|
29
|
+
expect(sm).toMatch(/font-weight:\s*var\(--umds-primitive-font-weight-extrabold\)/);
|
|
30
|
+
const topo = bloco(".umds-stat-card--rotulo-topo .umds-stat-card__valor-secundario");
|
|
31
|
+
expect(topo).toMatch(/font-size:\s*var\(--umds-primitive-font-size-lg\)/);
|
|
32
|
+
expect(topo).toMatch(/font-weight:\s*var\(--umds-primitive-font-weight-extrabold\)/);
|
|
33
|
+
});
|
|
26
34
|
it("o número continua antes do rótulo no HTML", () => {
|
|
27
35
|
const { container } = render(_jsx(StatCard, { value: 44, label: "N\u00E3o lidos", size: "sm" }));
|
|
28
36
|
const valor = container.querySelector(".umds-stat-card__valor");
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@1doc/1ds-react",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.14.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"sideEffects": [
|
|
6
6
|
"**/*.css"
|
|
@@ -45,8 +45,8 @@
|
|
|
45
45
|
"react-dom": "^18.0.0",
|
|
46
46
|
"typescript": "^5.0.0",
|
|
47
47
|
"vitest": "^3.2.7",
|
|
48
|
-
"@1doc/1ds-
|
|
49
|
-
"@1doc/1ds-
|
|
48
|
+
"@1doc/1ds-core": "2.14.0",
|
|
49
|
+
"@1doc/1ds-tokens": "0.7.0"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public"
|
package/src/index.ts
CHANGED
|
@@ -58,6 +58,8 @@ export { default as Input } from "./components/Input/Input";
|
|
|
58
58
|
export { default as Kanban } from "./components/Kanban/Kanban";
|
|
59
59
|
export { default as KanbanCard } from "./components/Kanban/KanbanCard";
|
|
60
60
|
export { default as KanbanColumn } from "./components/Kanban/KanbanColumn";
|
|
61
|
+
export { default as Counter } from "./components/Counter/Counter";
|
|
62
|
+
export type * from "./components/Counter/Counter";
|
|
61
63
|
export { default as Kbd } from "./components/Kbd/Kbd";
|
|
62
64
|
export { default as Link } from "./components/Link/Link";
|
|
63
65
|
export { default as LineChart } from "./components/Chart/LineChart";
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from "vitest";
|
|
2
|
+
import { act, fireEvent, render, screen } from "@testing-library/react";
|
|
3
|
+
import Button from "../components/Button/Button";
|
|
4
|
+
import Counter from "../components/Counter/Counter";
|
|
5
|
+
import Dropdown from "../components/Dropdown/Dropdown";
|
|
6
|
+
import ListItem from "../components/List/ListItem";
|
|
7
|
+
import StatCard from "../components/StatCard/StatCard";
|
|
8
|
+
|
|
9
|
+
describe("Counter", () => {
|
|
10
|
+
it("sem animação mostra o valor final formatado em pt-BR", () => {
|
|
11
|
+
const { container } = render(<Counter value={1284} disableAnimation />);
|
|
12
|
+
expect(container.querySelector(".umds-counter__numero")).toHaveTextContent("1.284");
|
|
13
|
+
expect(container.querySelector(".umds-counter__numero")).toHaveAttribute("aria-hidden", "true");
|
|
14
|
+
expect(container.querySelector(".umds-counter__leitura")).toHaveTextContent("1.284");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("casas decimais seguem o value; prefixo e sufixo entram no texto", () => {
|
|
18
|
+
const { container } = render(
|
|
19
|
+
<>
|
|
20
|
+
<Counter value={22.31} suffix="%" disableAnimation />
|
|
21
|
+
<Counter value={15890.5} prefix="R$ " decimals={2} disableAnimation />
|
|
22
|
+
</>,
|
|
23
|
+
);
|
|
24
|
+
const leituras = container.querySelectorAll(".umds-counter__leitura");
|
|
25
|
+
expect(leituras[0]).toHaveTextContent("22,31%");
|
|
26
|
+
expect(leituras[1].textContent).toBe("R$ 15.890,50");
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it("conta do from até o valor e termina no valor exato", async () => {
|
|
30
|
+
vi.useFakeTimers();
|
|
31
|
+
const { container } = render(<Counter value={500} duration={100} />);
|
|
32
|
+
const numero = () => container.querySelector(".umds-counter__numero")!.textContent;
|
|
33
|
+
expect(numero()).toBe("0");
|
|
34
|
+
await act(async () => {
|
|
35
|
+
vi.advanceTimersByTime(500);
|
|
36
|
+
});
|
|
37
|
+
expect(numero()).toBe("500");
|
|
38
|
+
// A reserva e a leitura já nascem com o valor final.
|
|
39
|
+
expect(container.querySelector(".umds-counter__leitura")).toHaveTextContent("500");
|
|
40
|
+
vi.useRealTimers();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
describe("ListItem · actions", () => {
|
|
45
|
+
it("as ações ficam no li, fora do elemento clicável", () => {
|
|
46
|
+
const { container } = render(
|
|
47
|
+
<ul>
|
|
48
|
+
<ListItem href="#doc" title="Memorando" actions={<button type="button">Mais</button>} />
|
|
49
|
+
</ul>,
|
|
50
|
+
);
|
|
51
|
+
const acao = screen.getByRole("button", { name: "Mais" });
|
|
52
|
+
expect(acao.closest("a")).toBeNull();
|
|
53
|
+
expect(acao.parentElement).toHaveClass("umds-list-item__actions");
|
|
54
|
+
expect(container.querySelector("li > a")).toHaveAttribute("href", "#doc");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("Button · href", () => {
|
|
59
|
+
it("com href é um link; desabilitado volta a ser botão", () => {
|
|
60
|
+
render(
|
|
61
|
+
<>
|
|
62
|
+
<Button href="#inbox">Inbox</Button>
|
|
63
|
+
<Button href="#cal" target="_blank">Calendário</Button>
|
|
64
|
+
<Button href="#fila" disabled>Fila</Button>
|
|
65
|
+
</>,
|
|
66
|
+
);
|
|
67
|
+
expect(screen.getByRole("link", { name: "Inbox" })).toHaveAttribute("href", "#inbox");
|
|
68
|
+
expect(screen.getByRole("link", { name: "Calendário" })).toHaveAttribute("rel", "noopener noreferrer");
|
|
69
|
+
expect(screen.getByRole("button", { name: "Fila" })).toBeDisabled();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("loading segura a navegação do link", () => {
|
|
73
|
+
render(<Button href="#inbox" loading loadingPosition="start">Inbox</Button>);
|
|
74
|
+
const link = screen.getByRole("link", { name: "Inbox" });
|
|
75
|
+
const evento = new MouseEvent("click", { bubbles: true, cancelable: true });
|
|
76
|
+
link.dispatchEvent(evento);
|
|
77
|
+
expect(evento.defaultPrevented).toBe(true);
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe("Dropdown · href", () => {
|
|
82
|
+
it("item com href vira <a role=menuitem>, e o onSelect continua", async () => {
|
|
83
|
+
const onSelect = vi.fn();
|
|
84
|
+
render(
|
|
85
|
+
<Dropdown
|
|
86
|
+
ariaLabel="Ações"
|
|
87
|
+
onSelect={onSelect}
|
|
88
|
+
items={[
|
|
89
|
+
{ key: "assinar", label: "Assinar", href: "#assinar" },
|
|
90
|
+
{ key: "arquivar", label: "Arquivar" },
|
|
91
|
+
]}
|
|
92
|
+
>
|
|
93
|
+
<button type="button">Abrir</button>
|
|
94
|
+
</Dropdown>,
|
|
95
|
+
);
|
|
96
|
+
fireEvent.click(screen.getByRole("button", { name: "Abrir" }));
|
|
97
|
+
const assinar = await screen.findByRole("menuitem", { name: "Assinar" });
|
|
98
|
+
expect(assinar.tagName).toBe("A");
|
|
99
|
+
expect(assinar).toHaveAttribute("href", "#assinar");
|
|
100
|
+
expect(screen.getByRole("menuitem", { name: "Arquivar" }).tagName).toBe("BUTTON");
|
|
101
|
+
fireEvent.click(assinar);
|
|
102
|
+
expect(onSelect).toHaveBeenCalledWith(expect.objectContaining({ key: "assinar" }));
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe("StatCard · loading e secondaryTone", () => {
|
|
107
|
+
it("loading troca os números por Skeleton e marca aria-busy", () => {
|
|
108
|
+
const { container } = render(
|
|
109
|
+
<StatCard value="22,31%" label="Qualidade" secondaryValue="14,11%" trendLabel="+2%" trend="up" loading />,
|
|
110
|
+
);
|
|
111
|
+
const cartao = container.querySelector(".umds-stat-card")!;
|
|
112
|
+
expect(cartao).toHaveAttribute("aria-busy", "true");
|
|
113
|
+
expect(cartao.textContent).not.toContain("22,31%");
|
|
114
|
+
expect(cartao.textContent).not.toContain("14,11%");
|
|
115
|
+
expect(cartao.textContent).not.toContain("+2%");
|
|
116
|
+
expect(cartao.textContent).toContain("Qualidade");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("secondaryTone escreve a classe; default não", () => {
|
|
120
|
+
const { container } = render(
|
|
121
|
+
<>
|
|
122
|
+
<StatCard value="1" secondaryValue="2" secondaryTone="positive" />
|
|
123
|
+
<StatCard value="1" secondaryValue="2" secondaryTone="default" />
|
|
124
|
+
</>,
|
|
125
|
+
);
|
|
126
|
+
expect(container.children[0]).toHaveClass("umds-stat-card--secundario-positive");
|
|
127
|
+
expect(container.children[1].className).not.toContain("secundario-");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -33,6 +33,15 @@ describe("StatCard · size", () => {
|
|
|
33
33
|
expect(container.firstElementChild).toHaveClass("umds-stat-card--sm", "umds-stat-card--rotulo-topo");
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
+
it("a segunda medida tem o mesmo tamanho e peso do número, no sm e no rótulo no topo", () => {
|
|
37
|
+
const sm = bloco(".umds-stat-card.umds-stat-card--sm .umds-stat-card__valor-secundario");
|
|
38
|
+
expect(sm).toMatch(/font-size:\s*1\.125rem/);
|
|
39
|
+
expect(sm).toMatch(/font-weight:\s*var\(--umds-primitive-font-weight-extrabold\)/);
|
|
40
|
+
const topo = bloco(".umds-stat-card--rotulo-topo .umds-stat-card__valor-secundario");
|
|
41
|
+
expect(topo).toMatch(/font-size:\s*var\(--umds-primitive-font-size-lg\)/);
|
|
42
|
+
expect(topo).toMatch(/font-weight:\s*var\(--umds-primitive-font-weight-extrabold\)/);
|
|
43
|
+
});
|
|
44
|
+
|
|
36
45
|
it("o número continua antes do rótulo no HTML", () => {
|
|
37
46
|
const { container } = render(<StatCard value={44} label="Não lidos" size="sm" />);
|
|
38
47
|
const valor = container.querySelector(".umds-stat-card__valor") as HTMLElement;
|