@1doc/1ds-react 2.10.0 → 2.11.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 (82) hide show
  1. package/CHANGELOG.md +219 -0
  2. package/dist/components/Accordion/Accordion.d.ts +6 -0
  3. package/dist/components/Accordion/Accordion.js +17 -5
  4. package/dist/components/Badge/Badge.css +13 -4
  5. package/dist/components/Badge/Badge.d.ts +9 -2
  6. package/dist/components/Badge/Badge.js +3 -0
  7. package/dist/components/Button/Button.css +5 -0
  8. package/dist/components/Button/Button.d.ts +3 -0
  9. package/dist/components/Button/Button.js +1 -0
  10. package/dist/components/Calendar/Calendar.css +10 -0
  11. package/dist/components/Calendar/Calendar.d.ts +3 -0
  12. package/dist/components/Calendar/Calendar.js +6 -1
  13. package/dist/components/Coachmark/Coachmark.d.ts +2 -1
  14. package/dist/components/Coachmark/Coachmark.js +6 -1
  15. package/dist/components/DataTable/DataTable.css +42 -0
  16. package/dist/components/DataTable/DataTable.d.ts +9 -2
  17. package/dist/components/DataTable/DataTable.js +15 -0
  18. package/dist/components/Input/Input.css +9 -0
  19. package/dist/components/Input/Input.d.ts +3 -0
  20. package/dist/components/Input/Input.js +1 -0
  21. package/dist/components/Modal/Modal.css +19 -0
  22. package/dist/components/Modal/Modal.d.ts +7 -0
  23. package/dist/components/Modal/Modal.js +1 -0
  24. package/dist/components/SegmentedControl/SegmentedControl.css +46 -0
  25. package/dist/components/SegmentedControl/SegmentedControl.d.ts +3 -0
  26. package/dist/components/SegmentedControl/SegmentedControl.js +1 -0
  27. package/dist/components/Select/Select.css +28 -2
  28. package/dist/components/Select/Select.d.ts +7 -0
  29. package/dist/components/Select/Select.js +55 -9
  30. package/dist/components/StatCard/StatCard.css +8 -0
  31. package/dist/components/StatCard/StatCard.d.ts +3 -0
  32. package/dist/components/StatCard/StatCard.js +1 -0
  33. package/dist/components/Tabs/Tabs.css +11 -0
  34. package/dist/components/Tabs/Tabs.d.ts +7 -0
  35. package/dist/components/Tabs/Tabs.js +7 -1
  36. package/dist/components/Tag/Tag.css +3 -1
  37. package/dist/teste/AccordionControlado.test.d.ts +1 -0
  38. package/dist/teste/AccordionControlado.test.js +73 -0
  39. package/dist/teste/BadgeAttachedTomEPadding.test.d.ts +1 -0
  40. package/dist/teste/BadgeAttachedTomEPadding.test.js +43 -0
  41. package/dist/teste/ButtonDensidade.test.d.ts +1 -0
  42. package/dist/teste/ButtonDensidade.test.js +28 -0
  43. package/dist/teste/CalendarCompacto.test.d.ts +1 -0
  44. package/dist/teste/CalendarCompacto.test.js +29 -0
  45. package/dist/teste/CoachmarkUmPasso.test.d.ts +1 -0
  46. package/dist/teste/CoachmarkUmPasso.test.js +34 -0
  47. package/dist/teste/DataTableCompactaETom.test.d.ts +1 -0
  48. package/dist/teste/DataTableCompactaETom.test.js +69 -0
  49. package/dist/teste/InputGhost.test.d.ts +1 -0
  50. package/dist/teste/InputGhost.test.js +32 -0
  51. package/dist/teste/ModalProporcoes1Doc.test.d.ts +1 -0
  52. package/dist/teste/ModalProporcoes1Doc.test.js +32 -0
  53. package/dist/teste/SegmentedControlSuperficie.test.d.ts +1 -0
  54. package/dist/teste/SegmentedControlSuperficie.test.js +39 -0
  55. package/dist/teste/SelectItemComEscopo.test.d.ts +1 -0
  56. package/dist/teste/SelectItemComEscopo.test.js +54 -0
  57. package/dist/teste/SelectListaFiltro.test.d.ts +1 -0
  58. package/dist/teste/SelectListaFiltro.test.js +102 -0
  59. package/dist/teste/StatCardTamanhoSm.test.d.ts +1 -0
  60. package/dist/teste/StatCardTamanhoSm.test.js +32 -0
  61. package/dist/teste/TabsCompacta.test.d.ts +1 -0
  62. package/dist/teste/TabsCompacta.test.js +35 -0
  63. package/dist/teste/TagPositivoTingido.test.d.ts +1 -0
  64. package/dist/teste/TagPositivoTingido.test.js +54 -0
  65. package/dist/teste/lerArquivo.d.ts +1 -0
  66. package/dist/teste/lerArquivo.js +12 -0
  67. package/package.json +3 -3
  68. package/src/teste/AccordionControlado.test.tsx +87 -0
  69. package/src/teste/BadgeAttachedTomEPadding.test.tsx +60 -0
  70. package/src/teste/ButtonDensidade.test.tsx +40 -0
  71. package/src/teste/CalendarCompacto.test.tsx +37 -0
  72. package/src/teste/CoachmarkUmPasso.test.tsx +45 -0
  73. package/src/teste/DataTableCompactaETom.test.tsx +85 -0
  74. package/src/teste/InputGhost.test.tsx +42 -0
  75. package/src/teste/ModalProporcoes1Doc.test.tsx +36 -0
  76. package/src/teste/SegmentedControlSuperficie.test.tsx +52 -0
  77. package/src/teste/SelectItemComEscopo.test.tsx +69 -0
  78. package/src/teste/SelectListaFiltro.test.tsx +120 -0
  79. package/src/teste/StatCardTamanhoSm.test.tsx +42 -0
  80. package/src/teste/TabsCompacta.test.tsx +45 -0
  81. package/src/teste/TagPositivoTingido.test.tsx +60 -0
  82. package/src/teste/lerArquivo.ts +13 -0
@@ -407,6 +407,25 @@
407
407
  background: var(--umds-semantic-color-primary-medium);
408
408
  }
409
409
 
410
+ /* ── density="1doc": as proporções do modal do CrisDoc ──
411
+ Cabeçalho de 67px (o padrão dá 61), rodapé de 65px (o padrão dá 73) e raio de 12px (o DS usa 8). Sem token
412
+ para essas medidas: literais comentados, como o resto do modal base. As alturas são mínimas, então título
413
+ com subtítulo ou botão mais alto continua crescendo. Fora a tela cheia e o painel, que têm cantos próprios. */
414
+ .umds-modal--1doc:not(.umds-modal--fullscreen):not(.umds-modal--janela-fullscreen):not(.umds-modal--sheet) {
415
+ /* stylelint-disable-next-line 1ds/raio-canonico -- o 12px é a opção density="1doc" (o modal do CrisDoc); o padrão segue em 8px */
416
+ border-radius: 12px;
417
+ }
418
+
419
+ .umds-modal--1doc.umds-modal--base .umds-modal__header {
420
+ align-items: center;
421
+ min-height: 67px;
422
+ }
423
+
424
+ .umds-modal--1doc.umds-modal--base .umds-modal__footer {
425
+ min-height: 65px;
426
+ padding: var(--umds-primitive-spacing-mini) var(--umds-primitive-spacing-xxs); /* 12px 24px */
427
+ }
428
+
410
429
  /* ── responsive: abaixo do breakpoint vira painel que sobe pela borda de baixo ──
411
430
  Mesma anatomia do BottomSheet (largura total, cantos de cima arredondados,
412
431
  alça, entrada por baixo). Quem decide o breakpoint é o componente (media
@@ -95,6 +95,13 @@ export interface ModalProps {
95
95
  * @default 'dialog'
96
96
  */
97
97
  layout?: "dialog" | "content";
98
+ /**
99
+ * Proporções do modal. `1doc` leva o cabeçalho a 67px, o rodapé a 65px e o raio a 12px, as medidas do
100
+ * modal do CrisDoc; o padrão (`default`) segue com 61px, 73px e 8px. Só o modal base (com título), e
101
+ * combina com `variant="primary"`. Tela cheia e painel (`responsive`) mantêm os cantos deles.
102
+ * @default 'default'
103
+ */
104
+ density?: "default" | "1doc";
98
105
  /** Classes CSS adicionais para o container do modal */
99
106
  className?: string;
100
107
  /** Se verdadeiro, renderiza o modal inline (sem fixed/overlay), útil para documentação */
@@ -149,6 +149,7 @@ function Modal(props) {
149
149
  `umds-modal--${props.variant || "default"}`,
150
150
  props.size && props.size !== "md" ? `umds-modal--${props.size}` : "",
151
151
  ehFeedback() ? "umds-modal--feedback" : "umds-modal--base",
152
+ props.density === "1doc" ? "umds-modal--1doc" : "",
152
153
  classeDaJanela(),
153
154
  props.responsive && isCompact ? "umds-modal--sheet" : "",
154
155
  props.className,
@@ -208,3 +208,49 @@
208
208
  background-color: var(--umds-semantic-color-neutral-light-pure);
209
209
  color: var(--umds-semantic-color-primary-pure);
210
210
  }
211
+
212
+ /* ── variant="surface": o escolhido em superfície branca (o seletor do CrisDoc) ──
213
+ Trilho cinza claro sem divisores; o segmento escolhido é uma peça branca com sombra e texto escuro em
214
+ negrito (a seleção não depende só do tom: o peso e a sombra a dizem). O trilho deixa de recortar, para a
215
+ sombra aparecer. O contador do escolhido fica cinza sobre o branco e o dos outros, branco sobre o trilho. */
216
+ .umds-segmented--surface .umds-segmented__track {
217
+ background-color: var(--umds-semantic-color-neutral-light-medium);
218
+ border-color: var(--umds-semantic-color-neutral-light-medium);
219
+ overflow: visible;
220
+ }
221
+
222
+ .umds-segmented--surface .umds-segmented__item {
223
+ background-color: transparent;
224
+ border-right-color: transparent;
225
+ border-radius: var(--umds-primitive-border-radius-sm);
226
+ }
227
+
228
+ .umds-segmented--surface .umds-segmented__item:hover {
229
+ background-color: var(--umds-semantic-color-neutral-light-down);
230
+ }
231
+
232
+ .umds-segmented--surface .umds-segmented__input:checked + .umds-segmented__item,
233
+ .umds-segmented--surface .umds-segmented__input:checked + .umds-segmented__item:hover {
234
+ background-color: var(--umds-semantic-color-background-white);
235
+ color: var(--umds-semantic-color-neutral-dark-pure);
236
+ font-weight: var(--umds-primitive-font-weight-bold);
237
+ box-shadow: var(--umds-semantic-shadow-md);
238
+ }
239
+
240
+ .umds-segmented--surface .umds-segmented__input:disabled + .umds-segmented__item:hover {
241
+ background-color: transparent;
242
+ }
243
+
244
+ .umds-segmented--surface .umds-segmented__input:disabled:checked + .umds-segmented__item:hover {
245
+ background-color: var(--umds-semantic-color-background-white);
246
+ color: var(--umds-semantic-color-neutral-dark-pure);
247
+ }
248
+
249
+ .umds-segmented--surface .umds-segmented__count {
250
+ background-color: var(--umds-semantic-color-background-white);
251
+ }
252
+
253
+ .umds-segmented--surface .umds-segmented__input:checked + .umds-segmented__item .umds-segmented__count {
254
+ background-color: var(--umds-semantic-color-neutral-light-medium);
255
+ color: var(--umds-semantic-color-neutral-dark-pure);
256
+ }
@@ -40,6 +40,7 @@ export interface SegmentedControlOption {
40
40
  * @property {boolean} invalid - Estado de erro.
41
41
  * @property {string} errorMessage - Mensagem de erro do grupo.
42
42
  * @property {string} size - Tamanho: 'sm' (32px) | 'md' (40px).
43
+ * @property {string} variant - 'default' (padrão): o segmento escolhido é azul cheio com texto branco. 'surface': trilho cinza claro e o segmento escolhido em superfície branca, com sombra e texto em negrito (o seletor do CrisDoc). O contador do escolhido fica cinza sobre o branco e o dos outros, branco sobre o trilho.
43
44
  * @property {boolean} fullWidth - Os segmentos dividem a largura do container.
44
45
  * @property {string} className - Classes CSS adicionais.
45
46
  * @property {function} onChange - Callback com o valor escolhido.
@@ -58,6 +59,8 @@ export interface SegmentedControlProps {
58
59
  invalid?: boolean;
59
60
  errorMessage?: string;
60
61
  size?: "sm" | "md";
62
+ /** `surface`: o escolhido em superfície branca sobre trilho cinza, no lugar do azul cheio. @default 'default' */
63
+ variant?: "default" | "surface";
61
64
  fullWidth?: boolean;
62
65
  className?: string;
63
66
  onChange?: (value: string) => void;
@@ -49,6 +49,7 @@ function SegmentedControl(props) {
49
49
  return [
50
50
  "umds-segmented",
51
51
  `umds-segmented--${props.size || "md"}`,
52
+ props.variant === "surface" ? "umds-segmented--surface" : "",
52
53
  props.fullWidth ? "umds-segmented--full-width" : "",
53
54
  props.disabled ? "umds-segmented--disabled" : "",
54
55
  isInvalid() ? "umds-segmented--invalid" : "",
@@ -175,8 +175,10 @@
175
175
 
176
176
  /* ── Variante filter: botão de filtro de barra de ferramentas ──
177
177
  Contornado em azul, largura do conteúdo, lista mais larga que o botão. */
178
+ /* fit-content e não auto: o wrapper é flex (bloco), e `auto` o estica até a largura do pai. Com ele esticado,
179
+ o `100%` da lista passava a ser o do pai (556px numa barra) e não o do gatilho. */
178
180
  .umds-select--filter {
179
- width: auto;
181
+ width: fit-content;
180
182
  }
181
183
 
182
184
  .umds-select--filter.umds-select--full-width {
@@ -224,12 +226,25 @@
224
226
  }
225
227
 
226
228
  .umds-select--filter .umds-select__dropdown {
227
- /* 240px: três vezes o degrau xxl (80px). Sem token de largura de menu. */
229
+ /* 240px: três vezes o degrau xxl (80px). Sem token de largura de menu. `listMinWidth` troca este mínimo
230
+ (estilo inline). A lista nunca é mais estreita que o gatilho (100%). */
228
231
  width: max-content;
229
232
  min-width: max(100%, calc(var(--umds-primitive-spacing-xxl) * 3));
230
233
  max-width: calc(100vw - var(--umds-primitive-spacing-xxxs) * 2);
231
234
  }
232
235
 
236
+ /* Perto da borda direita da janela: a lista se alinha ao fim do gatilho (`alignEnd`, medido ao abrir). */
237
+ .umds-select--filter.umds-select--list-end .umds-select__dropdown {
238
+ left: auto;
239
+ right: 0;
240
+ }
241
+
242
+ /* A busca da lista de filtro acompanha a largura da lista e não a dita: `contain: inline-size` tira a
243
+ largura natural do campo da conta do `max-content`. */
244
+ .umds-select--filter .umds-select__search-container {
245
+ contain: inline-size;
246
+ }
247
+
233
248
  /* ── Variante search: o filtro dentro de uma barra de busca ──
234
249
  Altura de campo e contorno neutro, para ficar ao lado do campo de busca. */
235
250
  .umds-select--filter-search .umds-select__trigger {
@@ -333,6 +348,7 @@
333
348
 
334
349
  .umds-select__search-input {
335
350
  flex: 1;
351
+ min-width: 0;
336
352
  border: none;
337
353
  outline: none;
338
354
  font-size: var(--umds-primitive-font-size-xxs);
@@ -557,3 +573,13 @@
557
573
  border-bottom-color: var(--umds-semantic-color-primary-pure);
558
574
  box-shadow: inset 0 -1px 0 var(--umds-semantic-color-primary-pure);
559
575
  }
576
+
577
+ /* ── item no gatilho: o conteúdo do slot/função `item` da opção escolhida ── */
578
+ .umds-select__trigger-item {
579
+ display: inline-flex;
580
+ align-items: center;
581
+ min-width: 0;
582
+ overflow: hidden;
583
+ font-size: var(--umds-semantic-field-text-size);
584
+ color: var(--umds-semantic-field-text-color);
585
+ }
@@ -53,6 +53,8 @@ export interface SelectOption {
53
53
  * @property {string} triggerSwatch - Amostra de cor à esquerda do texto do gatilho: um token (`var(--umds-semantic-color-...)`) ou `var(--umds-primitive-...)`. Decorativa; o texto diz a opção.
54
54
  * @property {string} emptyOptionLabel - Opção "sem filtro" no topo da lista (seleção única): marcada quando nada está escolhido; ao escolher, `onChange` recebe `""` e o campo volta a mostrar o `placeholder` (o nome do filtro).
55
55
  * @property {any} footer - Rodapé fixo da lista, com ações ("Gerenciar marcadores"). Passe o elemento correspondente (React node ou Vue slot `footer`).
56
+ * @property {any} item - Conteúdo de cada opção na lista, e do gatilho com a opção escolhida (seleção única). No React, uma função `({ optionData }) => nó` (`optionData` é a opção: `label`, `value` e os campos seus). No Vue, o slot com escopo `#item="{ optionData }"` (o mesmo `optionData`; `option` é um apelido). Sem ela, a opção é o `label` em texto. Na lista de seleção múltipla e na do `emptyOptionLabel` o gatilho segue mostrando o texto.
57
+ * @property {string|number} listMinWidth - Largura mínima da lista das variantes de filtro: um número (px) ou uma medida CSS ("220px"). Padrão: 240px. A lista tem a largura do gatilho (nunca menor), cresce até o conteúdo e nunca passa da borda da janela: perto da borda direita ela se alinha ao fim do gatilho. A busca interna acompanha a largura da lista, sem alargá-la.
56
58
  * @quandoUsar Escolher UM valor de uma lista fechada. O valor selecionado fica visível no campo.
57
59
  * @quandoNaoUsar Para escolher VÁRIOS valores — use `MultiSelect`. Para um menu de AÇÕES (editar, excluir, duplicar) em vez de valores, use `Dropdown`. Para 2 a 4 opções mutuamente exclusivas sempre visíveis, use `Radio`.
58
60
  */
@@ -110,6 +112,11 @@ export interface SelectProps {
110
112
  emptyOptionLabel?: string;
111
113
  /** Rodapé fixo da lista (ações). React node ou slot `footer` no Vue. */
112
114
  footer?: any;
115
+ /**
116
+ * Largura mínima da lista das variantes de filtro: número (px) ou medida CSS ("220px"). A lista nunca
117
+ * fica mais estreita que o gatilho. @default 240 (o `filter` do DS)
118
+ */
119
+ listMinWidth?: string | number;
113
120
  pagination?: boolean;
114
121
  hasMore?: boolean;
115
122
  onLoadMore?: () => void;
@@ -3,7 +3,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-run
3
3
  import './Select.css';
4
4
  import { useState, useRef, useEffect } from "react";
5
5
  import { faChevronUp, faChevronDown, faMagnifyingGlass, faXmark, faCheck, faTrash, } from "@fortawesome/free-solid-svg-icons";
6
- import { computePosition, autoUpdate, offset, flip, size, } from "@floating-ui/dom";
6
+ import { computePosition, autoUpdate, offset, flip, shift, size, } from "@floating-ui/dom";
7
7
  import Text from "../Text/Text";
8
8
  import Icon from "../Icon/Icon";
9
9
  import Spinner from "../Spinner/Spinner";
@@ -38,6 +38,7 @@ function Select(props) {
38
38
  const [floatW, setFloatW] = useState(() => 0);
39
39
  const [floatMaxH, setFloatMaxH] = useState(() => 0);
40
40
  const [listMaxH, setListMaxH] = useState(() => 0);
41
+ const [alignEnd, setAlignEnd] = useState(() => false);
41
42
  const [searchTerm, setSearchTerm] = useState(() => "");
42
43
  const [internalSelected, setInternalSelected] = useState(() => Array.isArray(props.defaultValue)
43
44
  ? props.defaultValue
@@ -150,6 +151,20 @@ function Select(props) {
150
151
  }
151
152
  return String(currentVal[0]);
152
153
  }
154
+ function opcaoDoGatilho() {
155
+ if (props.multiple || isPlaceholder())
156
+ return undefined;
157
+ return selectedOptionsList()[0];
158
+ }
159
+ function itemNoGatilho() {
160
+ return typeof props.item === "function" && !!opcaoDoGatilho();
161
+ }
162
+ function conteudoDoItemNoGatilho() {
163
+ return props.item({
164
+ optionData: opcaoDoGatilho(),
165
+ option: opcaoDoGatilho(),
166
+ });
167
+ }
153
168
  function triggerSwatchColor() {
154
169
  if (props.triggerSwatch)
155
170
  return props.triggerSwatch;
@@ -216,6 +231,7 @@ function Select(props) {
216
231
  props.variant === "search" ? "umds-select--filter-search" : "",
217
232
  props.variant === "combo" ? "umds-select--filter-combo" : "",
218
233
  opened() && floatMode ? "umds-select--floating" : "",
234
+ opened() && alignEnd ? "umds-select--list-end" : "",
219
235
  props.triggerIcon ? "umds-select--has-trigger-icon" : "",
220
236
  opensUp && props.searchVariant !== "inline"
221
237
  ? "umds-select--placement-top"
@@ -307,6 +323,7 @@ function Select(props) {
307
323
  const flutuante = insideDialog();
308
324
  setFloatMode(flutuante);
309
325
  setFloatReady(false);
326
+ setAlignEnd(false);
310
327
  setOpensUp(shouldOpenUp(flutuante));
311
328
  // Abre com a opção já selecionada ativa (padrão combobox).
312
329
  setActiveIndex(filteredOptionsList().findIndex((opt) => isOptionSelected(opt.value)));
@@ -326,6 +343,10 @@ function Select(props) {
326
343
  flip({
327
344
  fallbackStrategy: "bestFit",
328
345
  }),
346
+ // Perto da borda da janela, a lista desliza para dentro (8px de respiro).
347
+ shift({
348
+ padding: 8,
349
+ }),
329
350
  size({
330
351
  apply({ availableHeight, rects }) {
331
352
  setFloatW(rects.reference.width);
@@ -339,6 +360,22 @@ function Select(props) {
339
360
  setFloatReady(true);
340
361
  });
341
362
  }
363
+ function listMinWidthCss() {
364
+ const pedida = props.listMinWidth;
365
+ if (!isFilter() || pedida === undefined || pedida === null || pedida === "")
366
+ return "";
367
+ return typeof pedida === "number" ? pedida + "px" : String(pedida);
368
+ }
369
+ function keepInWindow() {
370
+ if (typeof window === "undefined" || !dropdownRef.current)
371
+ return;
372
+ if (!opened() || !isFilter() || floatMode || alignEnd)
373
+ return;
374
+ const caixa = dropdownRef.current.getBoundingClientRect();
375
+ // 8px de respiro até a borda: o degrau nano do DS.
376
+ if (caixa.right > window.innerWidth - 8)
377
+ setAlignEnd(true);
378
+ }
342
379
  function dropdownStyle() {
343
380
  if (props.searchVariant === "inline")
344
381
  return {
@@ -355,7 +392,11 @@ function Select(props) {
355
392
  };
356
393
  if (isFilter()) {
357
394
  estilo.minWidth =
358
- "max(" + floatW + "px, calc(var(--umds-primitive-spacing-xxl) * 3))";
395
+ "max(" +
396
+ floatW +
397
+ "px, " +
398
+ (listMinWidthCss() || "calc(var(--umds-primitive-spacing-xxl) * 3)") +
399
+ ")";
359
400
  }
360
401
  else {
361
402
  estilo.width = floatW + "px";
@@ -364,11 +405,12 @@ function Select(props) {
364
405
  estilo.maxHeight = floatMaxH + "px";
365
406
  return estilo;
366
407
  }
367
- return listMaxH
368
- ? {
369
- maxHeight: listMaxH + "px",
370
- }
371
- : {};
408
+ const estilo = {};
409
+ if (listMaxH)
410
+ estilo.maxHeight = listMaxH + "px";
411
+ if (listMinWidthCss())
412
+ estilo.minWidth = "max(100%, " + listMinWidthCss() + ")";
413
+ return estilo;
372
414
  }
373
415
  function toggle() {
374
416
  if (props.disabled || props.readOnly)
@@ -575,6 +617,7 @@ function Select(props) {
575
617
  latestOpenChangeRef.current = props.onOpenChange;
576
618
  if (opened() && !prepared)
577
619
  prepareOpen();
620
+ keepInWindow();
578
621
  if (typeof document === "undefined")
579
622
  return;
580
623
  const handleClickOutside = (event) => {
@@ -600,6 +643,9 @@ function Select(props) {
600
643
  prepareOpen();
601
644
  if (!opened() && prepared)
602
645
  setPrepared(false);
646
+ if (!opened() && alignEnd)
647
+ setAlignEnd(false);
648
+ keepInWindow();
603
649
  // Lista flutuante: segue o gatilho (scroll, resize) enquanto aberta.
604
650
  if (opened() &&
605
651
  floatMode &&
@@ -649,7 +695,7 @@ function Select(props) {
649
695
  }, []);
650
696
  return (_jsxs("div", { ref: selectRef, className: wrapperClassesList(), children: [props.label ? (_jsxs(Text, { as: "label", variant: "label", className: "umds-select__label", id: labelIdText(), children: [props.label, props.required ? (_jsx("span", { className: "umds-select__required", children: " *" })) : null] })) : null, _jsxs("div", { className: "umds-select__container", children: [!(opened() && props.searchVariant === "inline") ? (_jsxs("div", { role: "combobox", "aria-haspopup": "listbox", className: "umds-select__trigger", ref: triggerRef, onClick: (event) => toggle(), onKeyDown: (event) => handleTriggerKeyDown(event), "aria-readonly": ariaReadonly(), "aria-expanded": opened(), "aria-controls": listboxIdText(), "aria-activedescendant": activeDescendantText(), "aria-labelledby": comboboxLabelledBy(), "aria-label": comboboxAriaLabel(), "aria-invalid": ariaInvalidText(), "aria-required": ariaRequiredText(), "aria-describedby": describedByText(), "aria-disabled": props.disabled ? "true" : undefined, tabIndex: props.disabled ? -1 : 0, children: [_jsxs("div", { className: "umds-select__values", children: [props.triggerIcon ? (_jsx("span", { "aria-hidden": "true", className: "umds-select__trigger-icon", children: _jsx(Icon, { variant: "inherit", customSize: "12px", icon: props.triggerIcon }) })) : null, triggerSwatchColor() ? (_jsx("span", { "aria-hidden": "true", className: "umds-select__swatch", style: {
651
697
  background: triggerSwatchColor(),
652
- } })) : null, displayLabelText() ? (_jsx(Text, { as: "span", variant: "body2", className: isPlaceholder() ? "umds-select__placeholder" : "", children: displayLabelText() })) : null, props.multiple &&
698
+ } })) : null, itemNoGatilho() ? (_jsx("span", { className: "umds-select__trigger-item", children: conteudoDoItemNoGatilho() })) : null, displayLabelText() && !itemNoGatilho() ? (_jsx(Text, { as: "span", variant: "body2", className: isPlaceholder() ? "umds-select__placeholder" : "", children: displayLabelText() })) : null, props.multiple &&
653
699
  props.selectionDisplay === "tags" &&
654
700
  selectedOptionsList().length > 0 ? (_jsx("div", { className: "umds-select__tags", children: selectedOptionsList()?.map((opt) => (_jsx(Tag, { size: "sm", label: opt.label, closable: true, disabled: props.disabled, onClose: (e) => removeValue(opt.value, e) }, opt.value))) })) : null] }), _jsxs("div", { className: "umds-select__actions", children: [props.loading ? _jsx(Spinner, { size: "sm", variant: "secondary" }) : null, _jsx("div", { className: "umds-select__arrow", children: _jsx(Icon, { size: "sm", icon: opened() ? faChevronUp : faChevronDown }) })] })] })) : null, opened() ? (_jsxs("div", { className: "umds-select__dropdown", ref: dropdownRef, style: dropdownStyle(), onKeyDown: (event) => handleDropdownKeyDown(event), children: [props.searchable ? (_jsxs("div", { className: "umds-select__search-container", children: [_jsx("div", { className: "umds-select__search-icon", children: _jsx(Icon, { size: "sm", icon: faMagnifyingGlass }) }), _jsx("input", { type: "text", className: "umds-select__search-input", ref: searchInputRef, placeholder: props.searchPlaceholder || "Buscar...", "aria-label": props.searchAriaLabel || "Buscar", value: searchTerm, onInput: (e) => handleSearch(e) }), props.searchVariant === "inline" ? (_jsx("button", { type: "button", className: "umds-select__search-close", "aria-label": props.closeSearchAriaLabel || "Fechar busca", onClick: (event) => closeSearch(), onKeyDown: (event) => handleSearchCloseKeyDown(event), children: _jsx(Icon, { size: "sm", icon: faXmark }) })) : null] })) : null, _jsxs("div", { role: "listbox", className: "umds-select__options", id: listboxIdText(), "aria-multiselectable": props.multiple ? "true" : undefined, onScroll: (e) => onScrollList(e), children: [filteredOptionsList().length === 0 && !props.loading ? (_jsx("div", { className: "umds-select__no-options", children: _jsx(Text, { variant: "body2", color: "secondary", children: props.emptyLabel || "Nenhum item encontrado" }) })) : null, filteredOptionsList()?.map((option, index) => (_jsxs("div", { role: "option", id: optionIdText(index), "aria-selected": isOptionSelected(option.value), onClick: (event) => selectOption(option), className: [
655
701
  "umds-select__option",
@@ -661,7 +707,7 @@ function Select(props) {
661
707
  .filter(Boolean)
662
708
  .join(" "), children: [props.multiple ? (_jsx("div", { className: "umds-select__option-checkbox", children: _jsx(Checkbox, { checked: isOptionSelected(option.value), decorative: true }) })) : null, option.color ? (_jsx("span", { "aria-hidden": "true", className: "umds-select__swatch", style: {
663
709
  background: option.color,
664
- } })) : null, _jsx("div", { className: "umds-select__option-content", children: _jsx(_Fragment, { children: props.item || (_jsx(Text, { variant: "body2", children: option.label })) }) }), !props.multiple && isOptionSelected(option.value) ? (_jsx("div", { className: "umds-select__option-check", children: _jsx(Icon, { size: "sm", icon: faCheck }) })) : null] }, option.value))), props.loading && props.pagination ? (_jsxs("div", { className: "umds-select__loading-more", children: [_jsx(Spinner, { size: "sm" }), _jsx(Text, { variant: "caption", children: props.loadingLabel || "Carregando mais..." })] })) : null] }), props.footer ? (_jsx("div", { className: "umds-select__footer", children: _jsx(_Fragment, { children: props.footer }) })) : null] })) : null] }), props.name ? (_jsx("input", { type: "hidden", name: props.name, value: formValue() || "" })) : null, props.errorMessage ? (_jsx(Text, { variant: "caption", color: "negative", className: "umds-select__error-message", id: errorIdText(), children: props.errorMessage })) : null, mostraApoio() ? (_jsx(Text, { variant: "caption", color: "secondary", className: "umds-select__description", id: helperIdText(), children: textoDeApoio() })) : null, props.multiple &&
710
+ } })) : null, _jsx("div", { className: "umds-select__option-content", children: _jsx(_Fragment, { children: typeof props.item === "function" ? props.item({ optionData: option, option }) : props.item || (_jsx(Text, { variant: "body2", children: option.label })) }) }), !props.multiple && isOptionSelected(option.value) ? (_jsx("div", { className: "umds-select__option-check", children: _jsx(Icon, { size: "sm", icon: faCheck }) })) : null] }, option.value))), props.loading && props.pagination ? (_jsxs("div", { className: "umds-select__loading-more", children: [_jsx(Spinner, { size: "sm" }), _jsx(Text, { variant: "caption", children: props.loadingLabel || "Carregando mais..." })] })) : null] }), props.footer ? (_jsx("div", { className: "umds-select__footer", children: _jsx(_Fragment, { children: props.footer }) })) : null] })) : null] }), props.name ? (_jsx("input", { type: "hidden", name: props.name, value: formValue() || "" })) : null, props.errorMessage ? (_jsx(Text, { variant: "caption", color: "negative", className: "umds-select__error-message", id: errorIdText(), children: props.errorMessage })) : null, mostraApoio() ? (_jsx(Text, { variant: "caption", color: "secondary", className: "umds-select__description", id: helperIdText(), children: textoDeApoio() })) : null, props.multiple &&
665
711
  props.selectionDisplay === "list" &&
666
712
  selectedOptionsList().length > 0 ? (_jsx("div", { className: "umds-select__selected-list", children: selectedOptionsList()?.map((opt) => (_jsxs("div", { className: "umds-select__selected-list-item", children: [_jsx(Text, { variant: "body2", children: opt.label }), _jsx("button", { type: "button", className: "umds-select__list-remove", "aria-label": removeItemAriaLabelText(opt.label), onClick: (e) => removeValue(opt.value, e), children: _jsx(Icon, { size: "sm", icon: faTrash }) })] }, opt.value))) })) : null] }));
667
713
  }
@@ -290,3 +290,11 @@
290
290
  .umds-stat-card--rotulo-topo.umds-stat-card--com-secundaria:not(:has(.umds-stat-card__icone)) .umds-stat-card__valor {
291
291
  grid-column: 1 / 3;
292
292
  }
293
+
294
+ /* ── size="sm": o KPI de 18px/800 ──
295
+ 18px não está na escala de fonte do DS (16 e 20): é a medida dos KPIs da home do CrisDoc, mantida como literal.
296
+ Dois seletores de classe para vencer o 32/800 do `labelPosition="top"` sem depender da ordem. */
297
+ .umds-stat-card.umds-stat-card--sm .umds-stat-card__valor {
298
+ font-size: 1.125rem; /* 18px */
299
+ font-weight: var(--umds-primitive-font-weight-extrabold);
300
+ }
@@ -22,6 +22,7 @@ import './StatCard.css';
22
22
  * @property {string} trendIntent - Se a variação é boa ou ruim (cor), independente da direção: 'positive' | 'negative' | 'neutral'
23
23
  * @property {string} trendLabel - Texto da variação (ex.: '12% em relação ao mês anterior')
24
24
  * @property {string} description - Texto auxiliar abaixo do rótulo
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"`.
25
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.
26
27
  * @property {string|number} secondaryValue - Segunda medida do cartão, menor, à direita da principal ("22,31%" do setor e "14,11%" do geral). Vem depois do resto no HTML. `0` aparece; vazio e ausente não.
27
28
  * @property {string} secondaryLabel - O que a segunda medida significa ("Geral")
@@ -73,6 +74,8 @@ export interface StatCardProps {
73
74
  secondaryValue?: string | number;
74
75
  /** O que a segunda medida significa. */
75
76
  secondaryLabel?: string;
77
+ /** Tamanho do número: `sm` é o KPI de 18px/800. @default 'md' */
78
+ size?: "md" | "sm";
76
79
  /** Cor do número principal. @default 'default' */
77
80
  valueTone?: "default" | "primary" | "positive" | "negative" | "info";
78
81
  /**
@@ -10,6 +10,7 @@ function StatCard(props) {
10
10
  "umds-stat-card",
11
11
  `umds-stat-card--${props.variant || "neutral"}`,
12
12
  props.align === "center" ? "umds-stat-card--centro" : "",
13
+ props.size === "sm" ? "umds-stat-card--sm" : "",
13
14
  props.labelPosition === "top" ? "umds-stat-card--rotulo-topo" : "",
14
15
  temSecundaria() ? "umds-stat-card--com-secundaria" : "",
15
16
  props.valueTone && props.valueTone !== "default"
@@ -93,6 +93,17 @@
93
93
  white-space: nowrap;
94
94
  }
95
95
 
96
+ /* ============================
97
+ Densidade compacta (density="compact" no Tabs)
98
+ Altura mínima de 40px (o degrau sm do DS) e padding vertical de 8px: com o texto de 14px e a barra de
99
+ 3px, a aba fica nos 40. Só o padding vertical (padding-block), para não desfazer o espaço do × da aba
100
+ fechável. O contador em pílula não muda: cinza na aba inativa e azul só na ativa.
101
+ ============================ */
102
+ .umds-tabs--compact .umds-tabs__tab {
103
+ min-height: var(--umds-primitive-spacing-sm); /* 40px */
104
+ padding-block: var(--umds-primitive-spacing-nano);
105
+ }
106
+
96
107
  /* ============================
97
108
  Label
98
109
  ============================ */
@@ -14,6 +14,13 @@ export interface TabsProps {
14
14
  onChange?: (index: string) => void;
15
15
  /** Label de acessibilidade para o tablist */
16
16
  ariaLabel?: string;
17
+ /**
18
+ * Densidade das abas: `compact` leva a altura mínima de 48px a 40px (padding vertical de 8px), para barras
19
+ * de abas dentro de uma tela (o CrisDoc usa 40). O contador em pílula não muda: cinza na aba inativa e azul
20
+ * só na ativa, nas duas densidades.
21
+ * @default 'default'
22
+ */
23
+ density?: "default" | "compact";
17
24
  /**
18
25
  * Identificador estável usado como base dos ids acessíveis
19
26
  * (`{id}-tab-{index}` / `{id}-panel-{index}`). Passe o MESMO id no
@@ -6,7 +6,13 @@ import Icon from "../Icon/Icon";
6
6
  import TabsContext from "./tabs.context";
7
7
  function Tabs(props) {
8
8
  function classes() {
9
- return ["umds-tabs", props.className].filter(Boolean).join(" ");
9
+ return [
10
+ "umds-tabs",
11
+ props.density === "compact" ? "umds-tabs--compact" : "",
12
+ props.className,
13
+ ]
14
+ .filter(Boolean)
15
+ .join(" ");
10
16
  }
11
17
  function barClasses() {
12
18
  return ["umds-tabs__bar", props.onAdd ? "umds-tabs__bar--com-add" : ""]
@@ -194,7 +194,9 @@
194
194
 
195
195
  .umds-tag--outlined.umds-tag--positive {
196
196
  border-color: var(--umds-semantic-color-feedback-positive-pure);
197
- color: var(--umds-semantic-color-feedback-positive-pure);
197
+ /* O contorno transparente deixa o texto sobre o fundo de quem está atrás (um card tingido de azul-claro).
198
+ positive-pure sobre primary-light dá 3,6:1; positive-dark dá 7,9:1 e é o par de texto do `soft`. */
199
+ color: var(--umds-semantic-color-feedback-positive-dark);
198
200
  }
199
201
 
200
202
  .umds-tag--outlined.umds-tag--negative {
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,73 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { useState } from "react";
4
+ import { fireEvent, render, screen } from "@testing-library/react";
5
+ import Accordion from "../components/Accordion/Accordion";
6
+ const ITENS = [
7
+ { id: "prazos", title: "Prazos", content: "Dez dias úteis.", defaultOpen: true },
8
+ { id: "anexos", title: "Anexos", content: "Até 10 MB." },
9
+ { id: "sigilo", title: "Sigilo", content: "Só o setor." },
10
+ ];
11
+ const aberto = (nome) => screen.getByRole("button", { name: nome }).getAttribute("aria-expanded") === "true";
12
+ describe("Accordion · não controlado (como antes)", () => {
13
+ it("começa pelos defaultOpen e o clique abre e fecha; onOpenChange avisa a nova lista", () => {
14
+ const aoMudar = vi.fn();
15
+ render(_jsx(Accordion, { items: ITENS, multiple: true, onOpenChange: aoMudar }));
16
+ expect(aberto("Prazos")).toBe(true);
17
+ fireEvent.click(screen.getByRole("button", { name: "Anexos" }));
18
+ expect(aberto("Anexos")).toBe(true);
19
+ expect(aoMudar).toHaveBeenLastCalledWith(["prazos", "anexos"]);
20
+ fireEvent.click(screen.getByRole("button", { name: "Prazos" }));
21
+ expect(aberto("Prazos")).toBe(false);
22
+ expect(aoMudar).toHaveBeenLastCalledWith(["anexos"]);
23
+ });
24
+ it("sem multiple, abrir um fecha o outro", () => {
25
+ render(_jsx(Accordion, { items: ITENS }));
26
+ fireEvent.click(screen.getByRole("button", { name: "Anexos" }));
27
+ expect(aberto("Anexos")).toBe(true);
28
+ expect(aberto("Prazos")).toBe(false);
29
+ });
30
+ });
31
+ describe("Accordion · controlado (open e onOpenChange)", () => {
32
+ it("open manda: o que está em open está aberto, ignorando defaultOpen", () => {
33
+ render(_jsx(Accordion, { items: ITENS, open: ["sigilo"] }));
34
+ expect(aberto("Sigilo")).toBe(true);
35
+ expect(aberto("Prazos")).toBe(false);
36
+ });
37
+ it("o clique só pede (onOpenChange); sem o app mudar open, nada abre", () => {
38
+ const aoMudar = vi.fn();
39
+ render(_jsx(Accordion, { items: ITENS, multiple: true, open: ["prazos"], onOpenChange: aoMudar }));
40
+ fireEvent.click(screen.getByRole("button", { name: "Anexos" }));
41
+ expect(aoMudar).toHaveBeenCalledWith(["prazos", "anexos"]);
42
+ expect(aberto("Anexos")).toBe(false);
43
+ });
44
+ it("o app guarda a escolha: com o estado dele, abre, fecha e a lista chega em ordem", () => {
45
+ let ultima = [];
46
+ function App() {
47
+ const [abertos, setAbertos] = useState([]);
48
+ return (_jsx(Accordion, { items: ITENS, multiple: true, open: abertos, onOpenChange: (ids) => {
49
+ ultima = ids;
50
+ setAbertos(ids);
51
+ } }));
52
+ }
53
+ render(_jsx(App, {}));
54
+ expect(aberto("Prazos")).toBe(false);
55
+ fireEvent.click(screen.getByRole("button", { name: "Sigilo" }));
56
+ fireEvent.click(screen.getByRole("button", { name: "Anexos" }));
57
+ expect(ultima).toEqual(["sigilo", "anexos"]);
58
+ expect(aberto("Sigilo") && aberto("Anexos")).toBe(true);
59
+ fireEvent.click(screen.getByRole("button", { name: "Sigilo" }));
60
+ expect(ultima).toEqual(["anexos"]);
61
+ expect(aberto("Sigilo")).toBe(false);
62
+ });
63
+ it("open vazio é controlado (tudo fechado), não ausência", () => {
64
+ render(_jsx(Accordion, { items: ITENS, open: [] }));
65
+ expect(aberto("Prazos")).toBe(false);
66
+ });
67
+ it("item desabilitado não pede nada", () => {
68
+ const aoMudar = vi.fn();
69
+ render(_jsx(Accordion, { items: [{ id: "x", title: "Bloqueado", content: "...", disabled: true }], open: [], onOpenChange: aoMudar }));
70
+ fireEvent.click(screen.getByRole("button", { name: "Bloqueado" }));
71
+ expect(aoMudar).not.toHaveBeenCalled();
72
+ });
73
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import { render } from "@testing-library/react";
4
+ import Badge from "../components/Badge/Badge";
5
+ import { lerArquivo } from "./lerArquivo";
6
+ const cssBadge = await lerArquivo("../core/src/components/Badge/Badge.css");
7
+ const css = cssBadge.replace(/\r\n/g, "\n");
8
+ const bloco = (seletor) => {
9
+ const inicio = css.indexOf(seletor + " {");
10
+ return css.slice(inicio, css.indexOf("}", inicio));
11
+ };
12
+ describe("Badge 2xs · padding lateral de 8px", () => {
13
+ it("a regra do 2xs usa o degrau nano (8px) dos lados e o micro (2px) em cima e embaixo", () => {
14
+ expect(bloco(".umds-badge--2xs")).toMatch(/padding:\s*var\(--umds-primitive-spacing-micro\)\s+var\(--umds-primitive-spacing-nano\)/);
15
+ });
16
+ it("os outros tamanhos não mudam de padding", () => {
17
+ expect(bloco(".umds-badge--xs")).toMatch(/padding:\s*var\(--umds-primitive-spacing-micro\)\s+var\(--umds-primitive-spacing-quarck\)/);
18
+ expect(bloco(".umds-badge--md")).toMatch(/padding:\s*var\(--umds-primitive-spacing-quarck\)\s+var\(--umds-primitive-spacing-nano\)/);
19
+ });
20
+ });
21
+ describe("Badge attached · 22px e tom escuro", () => {
22
+ it("o rótulo colado tem altura mínima de 22px (24 - 2), de qualquer tamanho", () => {
23
+ expect(bloco(".umds-badge--attached")).toMatch(/min-height:\s*calc\(var\(--umds-primitive-spacing-xxs\)\s*-\s*var\(--umds-primitive-spacing-micro\)\)/);
24
+ const { container } = render(_jsx(Badge, { attached: true, size: "xs", variant: "neutral", appearance: "solid", children: "Tarefa manual" }));
25
+ expect(container.firstElementChild).toHaveClass("umds-badge--attached", "umds-badge--xs");
26
+ });
27
+ it("attachedTone dark escreve a classe; sem ela e com light, não", () => {
28
+ const { container } = render(_jsxs(_Fragment, { children: [_jsx(Badge, { attached: true, attachedTone: "dark", variant: "neutral", appearance: "solid", children: "A" }), _jsx(Badge, { attached: true, variant: "neutral", appearance: "solid", children: "B" }), _jsx(Badge, { attached: true, attachedTone: "light", variant: "neutral", appearance: "solid", children: "C" })] }));
29
+ expect(container.children[0]).toHaveClass("umds-badge--attached-dark");
30
+ expect(container.children[1]).not.toHaveClass("umds-badge--attached-dark");
31
+ expect(container.children[2]).not.toHaveClass("umds-badge--attached-dark");
32
+ });
33
+ it("sem attached o tom é ignorado", () => {
34
+ const { container } = render(_jsx(Badge, { attachedTone: "dark", variant: "neutral", appearance: "solid", children: "A" }));
35
+ expect(container.firstElementChild).not.toHaveClass("umds-badge--attached-dark");
36
+ });
37
+ it("CSS: o escuro é neutral-dark-down com texto branco, só no neutro sólido colado", () => {
38
+ const regra = bloco(".umds-badge--attached.umds-badge--neutral.umds-badge--solid.umds-badge--attached-dark");
39
+ expect(regra).toMatch(/background-color:\s*var\(--umds-semantic-color-neutral-dark-down\)/);
40
+ expect(regra).toMatch(/color:\s*var\(--umds-semantic-color-neutral-light-pure\)/);
41
+ expect(bloco(".umds-badge--attached.umds-badge--neutral.umds-badge--solid")).toMatch(/neutral-light-medium/);
42
+ });
43
+ });
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,28 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import { render, screen } from "@testing-library/react";
4
+ import Button from "../components/Button/Button";
5
+ import { lerArquivo } from "./lerArquivo";
6
+ const cssButton = await lerArquivo("../core/src/components/Button/Button.css");
7
+ const css = cssButton.replace(/\r\n/g, "\n");
8
+ const bloco = (seletor) => {
9
+ const inicio = css.indexOf(seletor + " {");
10
+ return css.slice(inicio, css.indexOf("}", inicio));
11
+ };
12
+ describe("Button · density", () => {
13
+ it("comfortable escreve a classe; sem a prop e com default o botão é o de antes", () => {
14
+ render(_jsxs(_Fragment, { children: [_jsx(Button, { size: "sm", density: "comfortable", children: "A" }), _jsx(Button, { size: "sm", children: "B" }), _jsx(Button, { size: "sm", density: "default", children: "C" })] }));
15
+ expect(screen.getByRole("button", { name: "A" })).toHaveClass("umds-button--sm", "umds-button--comfortable");
16
+ expect(screen.getByRole("button", { name: "B" })).not.toHaveClass("umds-button--comfortable");
17
+ expect(screen.getByRole("button", { name: "C" })).not.toHaveClass("umds-button--comfortable");
18
+ });
19
+ it("CSS: o sm confortável tem 12px de padding lateral; o sm de antes segue em 8px; o só-ícone fica de fora", () => {
20
+ expect(bloco(".umds-button--sm.umds-button--comfortable:not(.umds-button--icon)")).toMatch(/padding:\s*0\s+var\(--umds-primitive-spacing-mini\)/);
21
+ expect(bloco(".umds-button--sm")).toMatch(/padding:\s*0\s+var\(--umds-primitive-spacing-nano\)/);
22
+ });
23
+ it("os outros tamanhos mantêm o padding (a densidade só mexe no sm)", () => {
24
+ expect(bloco(".umds-button--xs")).toMatch(/padding:\s*0\s+var\(--umds-primitive-spacing-mini\)/);
25
+ expect(bloco(".umds-button--md")).toMatch(/padding:\s*0\s+var\(--umds-primitive-spacing-xxxs\)/);
26
+ expect(bloco(".umds-button--lg")).toMatch(/padding:\s*0\s+var\(--umds-primitive-spacing-xxs\)/);
27
+ });
28
+ });