@1doc/1ds-react 2.12.2 → 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.
Files changed (33) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/components/Breadcrumb/Breadcrumb.css +50 -0
  3. package/dist/components/Breadcrumb/Breadcrumb.d.ts +9 -0
  4. package/dist/components/Breadcrumb/Breadcrumb.js +11 -2
  5. package/dist/components/Button/Button.css +6 -1
  6. package/dist/components/Button/Button.d.ts +11 -2
  7. package/dist/components/Button/Button.js +15 -3
  8. package/dist/components/Counter/Counter.css +43 -0
  9. package/dist/components/Counter/Counter.d.ts +57 -0
  10. package/dist/components/Counter/Counter.js +99 -0
  11. package/dist/components/Dropdown/Dropdown.css +2 -0
  12. package/dist/components/Dropdown/Dropdown.d.ts +9 -0
  13. package/dist/components/Dropdown/Dropdown.js +20 -2
  14. package/dist/components/List/List.css +47 -0
  15. package/dist/components/List/ListItem.d.ts +10 -0
  16. package/dist/components/List/ListItem.js +1 -1
  17. package/dist/components/StatCard/StatCard.css +57 -2
  18. package/dist/components/StatCard/StatCard.d.ts +12 -6
  19. package/dist/components/StatCard/StatCard.js +6 -2
  20. package/dist/components/Tooltip/Tooltip.css +3 -0
  21. package/dist/components/Tooltip/Tooltip.js +33 -2
  22. package/dist/index.d.ts +2 -0
  23. package/dist/index.js +1 -0
  24. package/dist/teste/BreadcrumbTruncado.test.d.ts +1 -0
  25. package/dist/teste/BreadcrumbTruncado.test.js +40 -0
  26. package/dist/teste/PedidosMatheus.test.d.ts +1 -0
  27. package/dist/teste/PedidosMatheus.test.js +91 -0
  28. package/dist/teste/StatCardTamanhoSm.test.js +8 -0
  29. package/package.json +2 -2
  30. package/src/index.ts +2 -0
  31. package/src/teste/BreadcrumbTruncado.test.tsx +52 -0
  32. package/src/teste/PedidosMatheus.test.tsx +129 -0
  33. package/src/teste/StatCardTamanhoSm.test.tsx +9 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,46 @@
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
+
22
+ ## 2.13.0
23
+
24
+ ### Minor Changes
25
+
26
+ - ab0ee2b: feat(breadcrumb): `truncate`, a trilha numa linha só
27
+
28
+ O `Breadcrumb` ganha `truncate`. A trilha passa a ocupar uma linha só e, quando não cabe, a página atual (o último item) encolhe
29
+ primeiro, com reticências, até 80px (separador incluído); só então os níveis anteriores encolhem. O separador nunca encolhe e o rótulo inteiro fica no
30
+ `title` de cada item. Feito para barras de altura fixa, como o `AppSubheader`, quando o último nível é um texto longo: o CrisDoc
31
+ mostra ali o número e o assunto do processo aberto, e sem isto a trilha quebrava em três linhas no celular. Sem a prop, nada muda.
32
+
33
+ ```vue
34
+ <Breadcrumb
35
+ truncate
36
+ :items="[
37
+ { label: '1Doc', href: '/' },
38
+ { label: 'Proc. Administrativo 412/2026' },
39
+ { label: assunto },
40
+ ]"
41
+ />
42
+ ```
43
+
3
44
  ## 2.12.2
4
45
 
5
46
  ### Patch Changes
@@ -70,3 +70,53 @@
70
70
  font-weight: 600;
71
71
  cursor: default;
72
72
  }
73
+
74
+ /* ── truncate: uma linha só, com reticências ──
75
+ A página atual encolhe primeiro: o `flex-shrink` alto faz o algoritmo do
76
+ flex tirar dela quase todo o espaço que falta, até a largura mínima; dali
77
+ em diante o que ainda falta se divide entre os níveis anteriores. O 100000
78
+ é uma razão de encolhimento, não uma medida, e precisa ser enorme: o flex
79
+ reparte o encolhimento na proporção (razão × largura), e com 100 os níveis
80
+ anteriores ainda levavam de 0,1 a 0,8px, o bastante para virar "1D…" e
81
+ "Emiss…" com espaço sobrando. O separador nunca encolhe.
82
+ Com rótulo final mais curto que o mínimo, o item ocupa o mínimo e a sobra
83
+ fica depois do texto, no fim da trilha, onde não aparece. */
84
+ .umds-breadcrumb--truncate {
85
+ min-width: 0;
86
+ max-width: 100%;
87
+ }
88
+
89
+ .umds-breadcrumb--truncate .umds-breadcrumb__list {
90
+ flex-wrap: nowrap;
91
+ }
92
+
93
+ .umds-breadcrumb--truncate .umds-breadcrumb__item {
94
+ min-width: 0;
95
+ white-space: nowrap;
96
+ }
97
+
98
+ .umds-breadcrumb--truncate .umds-breadcrumb__item:last-child {
99
+ flex-shrink: 100000;
100
+ min-width: var(--umds-primitive-spacing-xxl); /* 80px com o separador: "Reforma…" ainda se lê */
101
+ }
102
+
103
+ .umds-breadcrumb--truncate .umds-breadcrumb__item:only-child {
104
+ min-width: 0;
105
+ }
106
+
107
+ .umds-breadcrumb--truncate .umds-breadcrumb__sep {
108
+ flex-shrink: 0;
109
+ }
110
+
111
+ .umds-breadcrumb--truncate .umds-breadcrumb__link,
112
+ .umds-breadcrumb--truncate .umds-breadcrumb__current {
113
+ display: block;
114
+ min-width: 0;
115
+ overflow: hidden;
116
+ text-overflow: ellipsis;
117
+ }
118
+
119
+ /* Em bloco, o alvo de 24px vem da altura da linha, e o texto fica centrado. */
120
+ .umds-breadcrumb--truncate .umds-breadcrumb__link {
121
+ line-height: var(--umds-primitive-spacing-xxs); /* 24px */
122
+ }
@@ -14,6 +14,7 @@ export interface BreadcrumbItem {
14
14
  * Breadcrumb component — navegação estrutural (trilha de páginas).
15
15
  * @property {BreadcrumbItem[]} items - Lista de itens da trilha; o último é a página atual
16
16
  * @property {string} separator - Separador entre itens (padrão '/')
17
+ * @property {boolean} truncate - Trilha numa linha só, com reticências quando não cabe
17
18
  * @property {string} className - Classes CSS adicionais
18
19
  * @quandoUsar Mostrar ONDE o usuário está na hierarquia do sistema e permitir subir de nível. Use em páginas com profundidade 2 ou mais.
19
20
  * @quandoNaoUsar Para progresso dentro de um fluxo de etapas — use `Stepper`. Para navegar entre seções da mesma página, use `Tabs`.
@@ -23,6 +24,14 @@ export interface BreadcrumbProps {
23
24
  ariaLabel?: string;
24
25
  items: BreadcrumbItem[];
25
26
  separator?: string;
27
+ /**
28
+ * Mantém a trilha numa linha só. Quando não cabe, a página atual (último item)
29
+ * encolhe primeiro, com reticências, até uma largura mínima; só então os
30
+ * níveis anteriores encolhem. O rótulo inteiro fica no `title` de cada item.
31
+ * Use em barras de altura fixa (subheader) e quando o último nível é um texto
32
+ * longo, como o assunto de um processo. @default false
33
+ */
34
+ truncate?: boolean;
26
35
  className?: string;
27
36
  }
28
37
  declare function Breadcrumb(props: BreadcrumbProps): import("react/jsx-runtime").JSX.Element;
@@ -6,11 +6,20 @@ function Breadcrumb(props) {
6
6
  return props.separator || "/";
7
7
  }
8
8
  function classes() {
9
- return ["umds-breadcrumb", props.className].filter(Boolean).join(" ");
9
+ return [
10
+ "umds-breadcrumb",
11
+ props.truncate ? "umds-breadcrumb--truncate" : "",
12
+ props.className,
13
+ ]
14
+ .filter(Boolean)
15
+ .join(" ");
10
16
  }
11
17
  function itemList() {
12
18
  return props.items || [];
13
19
  }
20
+ function titleOf(item) {
21
+ return props.truncate ? item.label : undefined;
22
+ }
14
23
  function isLast(index) {
15
24
  return index === (props.items || []).length - 1;
16
25
  }
@@ -24,6 +33,6 @@ function Breadcrumb(props) {
24
33
  }
25
34
  }
26
35
  }
27
- return (_jsx("nav", { "aria-label": props.ariaLabel || "Navegação estrutural", className: classes(), children: _jsx("ol", { className: "umds-breadcrumb__list", children: itemList()?.map((item, index) => (_jsxs("li", { className: "umds-breadcrumb__item", children: [index > 0 ? (_jsx("span", { "aria-hidden": "true", className: "umds-breadcrumb__sep", children: sep() })) : null, !isLast(index) && !!item.href ? (_jsx("a", { className: "umds-breadcrumb__link", href: item.href, children: item.label })) : null, !isLast(index) && !item.href ? (_jsx("span", { role: "button", className: "umds-breadcrumb__link", tabIndex: 0, onClick: (event) => item.onClick && item.onClick(), onKeyDown: (event) => handleItemKey(event, item), children: item.label })) : null, isLast(index) ? (_jsx("span", { "aria-current": "page", className: "umds-breadcrumb__current", children: item.label })) : null] }, index))) }) }));
36
+ return (_jsx("nav", { "aria-label": props.ariaLabel || "Navegação estrutural", className: classes(), children: _jsx("ol", { className: "umds-breadcrumb__list", children: itemList()?.map((item, index) => (_jsxs("li", { className: "umds-breadcrumb__item", children: [index > 0 ? (_jsx("span", { "aria-hidden": "true", className: "umds-breadcrumb__sep", children: sep() })) : null, !isLast(index) && !!item.href ? (_jsx("a", { className: "umds-breadcrumb__link", href: item.href, title: titleOf(item), children: item.label })) : null, !isLast(index) && !item.href ? (_jsx("span", { role: "button", className: "umds-breadcrumb__link", tabIndex: 0, title: titleOf(item), onClick: (event) => item.onClick && item.onClick(), onKeyDown: (event) => handleItemKey(event, item), children: item.label })) : null, isLast(index) ? (_jsx("span", { "aria-current": "page", className: "umds-breadcrumb__current", title: titleOf(item), children: item.label })) : null] }, index))) }) }));
28
37
  }
29
38
  export default Breadcrumb;
@@ -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: var(--umds-semantic-color-neutral-light-medium);
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
- * @quandoUsar AÇÃO que muda estado: salvar, enviar, abrir modal, confirmar. Use `variant="icon"` para ação só com ícone (sempre com `ariaLabel`).
26
- * @quandoNaoUsar Para NAVEGAR para outra página ou recurso — use `Link`, que gera `<a>` e permite abrir em nova aba. A regra: se o usuário pode querer "abrir em nova aba", é `Link`.
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
- if (props.loading)
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. */