@1doc/1ds-react 2.9.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 (172) hide show
  1. package/CHANGELOG.md +653 -0
  2. package/dist/components/Accordion/Accordion.css +13 -0
  3. package/dist/components/Accordion/Accordion.d.ts +9 -0
  4. package/dist/components/Accordion/Accordion.js +18 -5
  5. package/dist/components/AppSubheader/AppSubheader.css +10 -1
  6. package/dist/components/AttachmentCard/AttachmentCard.css +10 -8
  7. package/dist/components/AttachmentCard/AttachmentCard.d.ts +3 -0
  8. package/dist/components/AttachmentCard/AttachmentCard.js +8 -1
  9. package/dist/components/Badge/Badge.css +27 -5
  10. package/dist/components/Badge/Badge.d.ts +20 -4
  11. package/dist/components/Badge/Badge.js +6 -0
  12. package/dist/components/Button/Button.css +5 -0
  13. package/dist/components/Button/Button.d.ts +3 -0
  14. package/dist/components/Button/Button.js +1 -0
  15. package/dist/components/Calendar/Calendar.css +10 -0
  16. package/dist/components/Calendar/Calendar.d.ts +10 -6
  17. package/dist/components/Calendar/Calendar.js +6 -1
  18. package/dist/components/Coachmark/Coachmark.css +10 -0
  19. package/dist/components/Coachmark/Coachmark.d.ts +14 -1
  20. package/dist/components/Coachmark/Coachmark.js +51 -5
  21. package/dist/components/ColorPicker/ColorPicker.css +8 -1
  22. package/dist/components/ColorPicker/ColorPicker.d.ts +2 -2
  23. package/dist/components/DataTable/DataTable.css +216 -0
  24. package/dist/components/DataTable/DataTable.d.ts +70 -2
  25. package/dist/components/DataTable/DataTable.js +293 -6
  26. package/dist/components/DescriptionList/DescriptionList.css +14 -0
  27. package/dist/components/DescriptionList/DescriptionList.d.ts +3 -0
  28. package/dist/components/DescriptionList/DescriptionList.js +1 -0
  29. package/dist/components/EmptyState/EmptyState.d.ts +4 -2
  30. package/dist/components/EmptyState/EmptyState.js +2 -2
  31. package/dist/components/Input/Input.css +30 -0
  32. package/dist/components/Input/Input.d.ts +10 -0
  33. package/dist/components/Input/Input.js +3 -1
  34. package/dist/components/List/ListItem.d.ts +14 -2
  35. package/dist/components/List/ListItem.js +3 -2
  36. package/dist/components/Modal/Modal.css +19 -0
  37. package/dist/components/Modal/Modal.d.ts +7 -0
  38. package/dist/components/Modal/Modal.js +1 -0
  39. package/dist/components/RichTextEditor/RichTextEditor.css +47 -0
  40. package/dist/components/RichTextEditor/RichTextEditor.d.ts +22 -8
  41. package/dist/components/RichTextEditor/RichTextEditor.js +460 -3
  42. package/dist/components/SegmentedControl/SegmentedControl.css +70 -0
  43. package/dist/components/SegmentedControl/SegmentedControl.d.ts +5 -0
  44. package/dist/components/SegmentedControl/SegmentedControl.js +5 -1
  45. package/dist/components/Select/Select.css +122 -2
  46. package/dist/components/Select/Select.d.ts +33 -4
  47. package/dist/components/Select/Select.js +256 -35
  48. package/dist/components/StatCard/StatCard.css +73 -0
  49. package/dist/components/StatCard/StatCard.d.ts +12 -0
  50. package/dist/components/StatCard/StatCard.js +11 -1
  51. package/dist/components/Tabs/Tabs.css +11 -0
  52. package/dist/components/Tabs/Tabs.d.ts +7 -0
  53. package/dist/components/Tabs/Tabs.js +7 -1
  54. package/dist/components/Tag/Tag.css +3 -1
  55. package/dist/components/Toast/Toast.css +32 -0
  56. package/dist/components/Toast/Toast.d.ts +18 -0
  57. package/dist/components/Toast/Toast.js +14 -1
  58. package/dist/components/Toast/ToastStack.js +8 -1
  59. package/dist/teste/AccordionControlado.test.d.ts +1 -0
  60. package/dist/teste/AccordionControlado.test.js +73 -0
  61. package/dist/teste/AccordionTituloNeutro.test.d.ts +1 -0
  62. package/dist/teste/AccordionTituloNeutro.test.js +23 -0
  63. package/dist/teste/AppSubheaderRegiao.test.d.ts +1 -0
  64. package/dist/teste/AppSubheaderRegiao.test.js +13 -0
  65. package/dist/teste/AttachmentCardLista.test.d.ts +1 -0
  66. package/dist/teste/AttachmentCardLista.test.js +49 -0
  67. package/dist/teste/AttachmentCardSm.test.d.ts +1 -0
  68. package/dist/teste/AttachmentCardSm.test.js +26 -0
  69. package/dist/teste/BadgeAttachedTomEPadding.test.d.ts +1 -0
  70. package/dist/teste/BadgeAttachedTomEPadding.test.js +43 -0
  71. package/dist/teste/BadgeNeutroColado.test.d.ts +1 -0
  72. package/dist/teste/BadgeNeutroColado.test.js +11 -0
  73. package/dist/teste/BadgeTag.test.js +13 -0
  74. package/dist/teste/ButtonDensidade.test.d.ts +1 -0
  75. package/dist/teste/ButtonDensidade.test.js +28 -0
  76. package/dist/teste/CalendarCabecalho.test.d.ts +1 -0
  77. package/dist/teste/CalendarCabecalho.test.js +11 -0
  78. package/dist/teste/CalendarCompacto.test.d.ts +1 -0
  79. package/dist/teste/CalendarCompacto.test.js +29 -0
  80. package/dist/teste/CoachmarkSemVeu.test.d.ts +1 -0
  81. package/dist/teste/CoachmarkSemVeu.test.js +85 -0
  82. package/dist/teste/CoachmarkUmPasso.test.d.ts +1 -0
  83. package/dist/teste/CoachmarkUmPasso.test.js +34 -0
  84. package/dist/teste/ColorPickerXs.test.d.ts +1 -0
  85. package/dist/teste/ColorPickerXs.test.js +21 -0
  86. package/dist/teste/DataTableArvoreEVariantes.test.d.ts +1 -0
  87. package/dist/teste/DataTableArvoreEVariantes.test.js +190 -0
  88. package/dist/teste/DataTableCompactaETom.test.d.ts +1 -0
  89. package/dist/teste/DataTableCompactaETom.test.js +69 -0
  90. package/dist/teste/DescriptionListCompacta.test.d.ts +1 -0
  91. package/dist/teste/DescriptionListCompacta.test.js +22 -0
  92. package/dist/teste/EmptyStateTituloRico.test.d.ts +1 -0
  93. package/dist/teste/EmptyStateTituloRico.test.js +23 -0
  94. package/dist/teste/InputAgrupado.test.d.ts +1 -0
  95. package/dist/teste/InputAgrupado.test.js +24 -0
  96. package/dist/teste/InputGhost.test.d.ts +1 -0
  97. package/dist/teste/InputGhost.test.js +32 -0
  98. package/dist/teste/ListItemTooltipStandalone.test.d.ts +1 -0
  99. package/dist/teste/ListItemTooltipStandalone.test.js +39 -0
  100. package/dist/teste/ModalProporcoes1Doc.test.d.ts +1 -0
  101. package/dist/teste/ModalProporcoes1Doc.test.js +32 -0
  102. package/dist/teste/RichTextEditorAlinhaImagemTabela.test.d.ts +1 -0
  103. package/dist/teste/RichTextEditorAlinhaImagemTabela.test.js +212 -0
  104. package/dist/teste/RichTextEditorEstiloDeTexto.test.d.ts +1 -0
  105. package/dist/teste/RichTextEditorEstiloDeTexto.test.js +120 -0
  106. package/dist/teste/SegmentedControlContador.test.d.ts +1 -0
  107. package/dist/teste/SegmentedControlContador.test.js +39 -0
  108. package/dist/teste/SegmentedControlSuperficie.test.d.ts +1 -0
  109. package/dist/teste/SegmentedControlSuperficie.test.js +39 -0
  110. package/dist/teste/SelectAbertoControladoEFlutuante.test.d.ts +1 -0
  111. package/dist/teste/SelectAbertoControladoEFlutuante.test.js +145 -0
  112. package/dist/teste/SelectFiltroCorETamanho.test.d.ts +1 -0
  113. package/dist/teste/SelectFiltroCorETamanho.test.js +77 -0
  114. package/dist/teste/SelectItemComEscopo.test.d.ts +1 -0
  115. package/dist/teste/SelectItemComEscopo.test.js +54 -0
  116. package/dist/teste/SelectListaFiltro.test.d.ts +1 -0
  117. package/dist/teste/SelectListaFiltro.test.js +102 -0
  118. package/dist/teste/StatCardSecundariaETom.test.d.ts +1 -0
  119. package/dist/teste/StatCardSecundariaETom.test.js +45 -0
  120. package/dist/teste/StatCardTamanhoSm.test.d.ts +1 -0
  121. package/dist/teste/StatCardTamanhoSm.test.js +32 -0
  122. package/dist/teste/TabsCompacta.test.d.ts +1 -0
  123. package/dist/teste/TabsCompacta.test.js +35 -0
  124. package/dist/teste/TagPositivoTingido.test.d.ts +1 -0
  125. package/dist/teste/TagPositivoTingido.test.js +54 -0
  126. package/dist/teste/ToastAcaoEDetalhes.test.d.ts +1 -0
  127. package/dist/teste/ToastAcaoEDetalhes.test.js +94 -0
  128. package/dist/teste/lerArquivo.d.ts +1 -0
  129. package/dist/teste/lerArquivo.js +12 -0
  130. package/dist/utils/html-seguro.d.ts +19 -1
  131. package/dist/utils/html-seguro.js +113 -2
  132. package/dist/utils/toast.d.ts +10 -1
  133. package/dist/utils/toast.js +2 -1
  134. package/package.json +3 -3
  135. package/src/teste/AccordionControlado.test.tsx +87 -0
  136. package/src/teste/AccordionTituloNeutro.test.tsx +25 -0
  137. package/src/teste/AppSubheaderRegiao.test.tsx +17 -0
  138. package/src/teste/AttachmentCardLista.test.tsx +53 -0
  139. package/src/teste/AttachmentCardSm.test.tsx +29 -0
  140. package/src/teste/BadgeAttachedTomEPadding.test.tsx +60 -0
  141. package/src/teste/BadgeNeutroColado.test.tsx +15 -0
  142. package/src/teste/BadgeTag.test.tsx +21 -0
  143. package/src/teste/ButtonDensidade.test.tsx +40 -0
  144. package/src/teste/CalendarCabecalho.test.tsx +11 -0
  145. package/src/teste/CalendarCompacto.test.tsx +37 -0
  146. package/src/teste/CoachmarkSemVeu.test.tsx +101 -0
  147. package/src/teste/CoachmarkUmPasso.test.tsx +45 -0
  148. package/src/teste/ColorPickerXs.test.tsx +22 -0
  149. package/src/teste/DataTableArvoreEVariantes.test.tsx +240 -0
  150. package/src/teste/DataTableCompactaETom.test.tsx +85 -0
  151. package/src/teste/DescriptionListCompacta.test.tsx +24 -0
  152. package/src/teste/EmptyStateTituloRico.test.tsx +34 -0
  153. package/src/teste/InputAgrupado.test.tsx +26 -0
  154. package/src/teste/InputGhost.test.tsx +42 -0
  155. package/src/teste/ListItemTooltipStandalone.test.tsx +64 -0
  156. package/src/teste/ModalProporcoes1Doc.test.tsx +36 -0
  157. package/src/teste/RichTextEditorAlinhaImagemTabela.test.tsx +238 -0
  158. package/src/teste/RichTextEditorEstiloDeTexto.test.tsx +137 -0
  159. package/src/teste/SegmentedControlContador.test.tsx +43 -0
  160. package/src/teste/SegmentedControlSuperficie.test.tsx +52 -0
  161. package/src/teste/SelectAbertoControladoEFlutuante.test.tsx +180 -0
  162. package/src/teste/SelectFiltroCorETamanho.test.tsx +90 -0
  163. package/src/teste/SelectItemComEscopo.test.tsx +69 -0
  164. package/src/teste/SelectListaFiltro.test.tsx +120 -0
  165. package/src/teste/StatCardSecundariaETom.test.tsx +51 -0
  166. package/src/teste/StatCardTamanhoSm.test.tsx +42 -0
  167. package/src/teste/TabsCompacta.test.tsx +45 -0
  168. package/src/teste/TagPositivoTingido.test.tsx +60 -0
  169. package/src/teste/ToastAcaoEDetalhes.test.tsx +109 -0
  170. package/src/teste/lerArquivo.ts +13 -0
  171. package/src/utils/html-seguro.ts +105 -2
  172. package/src/utils/toast.ts +12 -2
@@ -5,13 +5,19 @@
5
5
  * fora (um valor guardado, um texto colado) pode trazer `<img onerror>`,
6
6
  * `<a href="javascript:...">`, `<script>`, `style` e classes de outro site. Este
7
7
  * módulo reduz o HTML ao que o editor sabe escrever: negrito, itálico,
8
- * sublinhado, tachado, listas, parágrafos, quebras, títulos, citação e link.
8
+ * sublinhado, tachado, listas, parágrafos, quebras, títulos, citação, link,
9
+ * estilo de texto por token (`color`, `font-size`, `font-family` em `<span>`),
10
+ * alinhamento de bloco (`text-align`), imagem (`http(s)`, caminho ou `data:image`
11
+ * em PNG, JPEG, GIF e WebP) e tabela simples.
9
12
  *
10
13
  * Roda no navegador (precisa de DOM). Dentro de um `<template>` o HTML é inerte:
11
14
  * nada executa e nenhuma imagem carrega enquanto se higieniza. Sem DOM
12
15
  * (servidor), devolve o texto escapado.
13
16
  */
14
- const PERMITIDAS = ["A", "B", "STRONG", "I", "EM", "U", "S", "STRIKE", "DEL", "UL", "OL", "LI", "P", "BR", "DIV", "SPAN", "H1", "H2", "H3", "H4", "BLOCKQUOTE"];
17
+ const PERMITIDAS = ["A", "B", "STRONG", "I", "EM", "U", "S", "STRIKE", "DEL", "UL", "OL", "LI", "P", "BR", "DIV", "SPAN", "H1", "H2", "H3", "H4", "BLOCKQUOTE", "IMG", "TABLE", "THEAD", "TBODY", "TR", "TH", "TD"];
18
+ /** Blocos que aceitam `text-align`. */
19
+ const ALINHAVEIS = ["P", "DIV", "H1", "H2", "H3", "H4", "LI", "BLOCKQUOTE", "TH", "TD"];
20
+ const ALINHAMENTOS = ["left", "center", "right", "justify"];
15
21
  const DESCARTADAS = ["SCRIPT", "STYLE", "IFRAME", "OBJECT", "EMBED", "LINK", "META", "TEMPLATE", "NOSCRIPT", "SVG", "MATH", "FORM", "INPUT", "BUTTON", "TEXTAREA", "SELECT"];
16
22
  const escapar = (texto) => texto.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
17
23
  /**
@@ -44,6 +50,68 @@ export const normalizarEndereco = (bruto) => {
44
50
  return "";
45
51
  return "https://" + texto;
46
52
  };
53
+ /** Cor, tamanho e família de fonte só valem como TOKEN do DS: `var(--umds-...)`. Cor em hex, rgb ou nome sai. */
54
+ const COR_TOKEN = /^var\(--umds-(?:primitive|semantic)-[a-z0-9-]+\)$/;
55
+ const TAMANHO_TOKEN = /^var\(--umds-primitive-font-size-[a-z0-9]+\)$/;
56
+ const FAMILIA_TOKEN = /^var\(--umds-(?:primitive|semantic)-font-family-(?:base|mono)\)$/;
57
+ /**
58
+ * Declaração de estilo de texto que o editor aceita: `color`, `font-size` e `font-family`,
59
+ * cada uma só com um token do DS. Devolve a declaração limpa ("color: var(--x);") ou "".
60
+ */
61
+ export const estiloDeTexto = (propriedade, valor) => {
62
+ const prop = String(propriedade || "").trim().toLowerCase();
63
+ const val = String(valor || "").trim();
64
+ if (prop === "color" && COR_TOKEN.test(val))
65
+ return "color: " + val + ";";
66
+ if (prop === "font-size" && TAMANHO_TOKEN.test(val))
67
+ return "font-size: " + val + ";";
68
+ if (prop === "font-family" && FAMILIA_TOKEN.test(val))
69
+ return "font-family: " + val + ";";
70
+ return "";
71
+ };
72
+ /** As declarações válidas de um atributo `style` (só color, font-size e font-family por token). */
73
+ const estiloDoSpan = (estilo) => {
74
+ const partes = String(estilo || "").split(";");
75
+ let saida = "";
76
+ for (let i = 0; i < partes.length; i++) {
77
+ const corte = partes[i].indexOf(":");
78
+ if (corte === -1)
79
+ continue;
80
+ const decl = estiloDeTexto(partes[i].slice(0, corte), partes[i].slice(corte + 1));
81
+ if (decl)
82
+ saida += (saida ? " " : "") + decl;
83
+ }
84
+ return saida;
85
+ };
86
+ /**
87
+ * Endereço de imagem que o editor aceita: `http(s)`, caminho (`/x`, `./x`, `../x`) ou
88
+ * `data:image` em base64 de PNG, JPEG, GIF ou WebP (SVG fica de fora: carrega script).
89
+ * Devolve o endereço limpo, ou "" se for perigoso ou vazio.
90
+ */
91
+ export const imagemSegura = (bruto) => {
92
+ const texto = (bruto || "").trim();
93
+ if (!texto)
94
+ return "";
95
+ const semControle = texto.replace(/[\u0000-\u001f\u007f\s]+/g, "");
96
+ if (/^data:image\/(png|jpe?g|gif|webp);base64,[a-z0-9+/]+=*$/i.test(semControle))
97
+ return semControle;
98
+ // `//host/x` (sem esquema) carregaria de outro site sem o autor ter pedido: fica de fora.
99
+ if (/^(https?:\/\/|\/(?!\/)|\.\.?\/)/i.test(semControle))
100
+ return texto;
101
+ return "";
102
+ };
103
+ /** O `text-align` de um elemento, pelo `style` ou pelo atributo `align`: left, center, right, justify ou "". */
104
+ const alinhamentoDe = (elemento) => {
105
+ const estilo = String(elemento.getAttribute("style") || "");
106
+ const achado = /(?:^|;)\s*text-align\s*:\s*([a-z]+)\s*(?:;|$)/i.exec(estilo);
107
+ const valor = (achado ? achado[1] : String(elemento.getAttribute("align") || "")).toLowerCase();
108
+ return ALINHAMENTOS.indexOf(valor) !== -1 ? valor : "";
109
+ };
110
+ /** Número inteiro de 2 a 99 em `colspan`/`rowspan`; o resto some. */
111
+ const vao = (valor) => {
112
+ const n = parseInt(String(valor || ""), 10);
113
+ return n >= 2 && n <= 99 ? String(n) : "";
114
+ };
47
115
  const limparNo = (no, saida, documento) => {
48
116
  const filhos = Array.prototype.slice.call(no.childNodes);
49
117
  for (let i = 0; i < filhos.length; i++) {
@@ -62,7 +130,36 @@ const limparNo = (no, saida, documento) => {
62
130
  limparNo(filho, saida, documento);
63
131
  continue;
64
132
  }
133
+ if (tag === "IMG") {
134
+ // Imagem sem endereço seguro some inteira (não deixa um <img> quebrado nem carregando de fora).
135
+ const origem = imagemSegura(filho.getAttribute("src") || "");
136
+ if (!origem)
137
+ continue;
138
+ const imagem = documento.createElement("img");
139
+ imagem.setAttribute("src", origem);
140
+ imagem.setAttribute("alt", String(filho.getAttribute("alt") || ""));
141
+ saida.appendChild(imagem);
142
+ continue;
143
+ }
65
144
  const novo = documento.createElement(tag.toLowerCase());
145
+ if (ALINHAVEIS.indexOf(tag) !== -1) {
146
+ const alinhamento = alinhamentoDe(filho);
147
+ if (alinhamento)
148
+ novo.setAttribute("style", "text-align: " + alinhamento + ";");
149
+ }
150
+ if (tag === "SPAN") {
151
+ const estiloValido = estiloDoSpan(filho.getAttribute("style") || "");
152
+ if (estiloValido)
153
+ novo.setAttribute("style", estiloValido);
154
+ }
155
+ if (tag === "TD" || tag === "TH") {
156
+ const colunas = vao(filho.getAttribute("colspan"));
157
+ const linhas = vao(filho.getAttribute("rowspan"));
158
+ if (colunas)
159
+ novo.setAttribute("colspan", colunas);
160
+ if (linhas)
161
+ novo.setAttribute("rowspan", linhas);
162
+ }
66
163
  if (tag === "A") {
67
164
  const href = enderecoSeguro(filho.getAttribute("href") || "");
68
165
  if (href) {
@@ -85,8 +182,22 @@ export const sanitizarHtml = (html) => {
85
182
  limparNo(molde.content, saida, document);
86
183
  return saida.innerHTML;
87
184
  };
185
+ /** A tabela vazia que o editor insere: `linhas` × `colunas` células com uma quebra, para o cursor entrar. */
186
+ export const htmlDaTabela = (linhas, colunas) => {
187
+ let celulas = "";
188
+ for (let c = 0; c < colunas; c++)
189
+ celulas += "<td><br></td>";
190
+ let corpo = "";
191
+ for (let l = 0; l < linhas; l++)
192
+ corpo += "<tr>" + celulas + "</tr>";
193
+ return "<table><tbody>" + corpo + "</tbody></table><p><br></p>";
194
+ };
195
+ /** O `<img>` que o editor insere (endereço já seguro). */
196
+ export const htmlDaImagem = (endereco, alt) => '<img src="' + escapar(endereco) + '" alt="' + escapar(alt || "") + '">';
88
197
  /** O editor está "vazio": sem texto, sem lista e sem quebra solta (o `<br>` que o navegador deixa). */
89
198
  export const htmlVazio = (html) => {
199
+ if (/<(img|table)[\s>]/i.test(html || ""))
200
+ return false;
90
201
  const texto = (html || "").replace(/<br\s*\/?>/gi, "").replace(/<\/?(div|p|span)[^>]*>/gi, "").trim();
91
202
  return texto === "";
92
203
  };
@@ -21,7 +21,13 @@ export interface ToastOptions {
21
21
  description?: string;
22
22
  /** Variante semântica @default 'info' */
23
23
  variant?: ToastVariant;
24
- /** Auto-dismiss em ms; 0 = não fecha sozinho @default 5000 */
24
+ /** Linhas de detalhe abaixo da descrição */
25
+ details?: string[];
26
+ /** Rótulo do botão de ação ("Desfazer"); só aparece com `onAction` */
27
+ actionLabel?: string;
28
+ /** Chamado ao acionar o botão de ação; depois dele o toast fecha */
29
+ onAction?: (event?: any) => void;
30
+ /** Auto-dismiss em ms; 0 = não fecha sozinho @default 5000 (0 quando há ação: dá tempo de agir, WCAG 2.2.1) */
25
31
  duration?: number;
26
32
  /** Exibe o botão de fechar @default true */
27
33
  closable?: boolean;
@@ -30,6 +36,9 @@ export interface ToastData {
30
36
  id: number;
31
37
  message: string;
32
38
  description?: string;
39
+ details?: string[];
40
+ actionLabel?: string;
41
+ onAction?: (event?: any) => void;
33
42
  variant: ToastVariant;
34
43
  duration: number;
35
44
  closable: boolean;
@@ -26,7 +26,8 @@ export const toast = (options) => {
26
26
  const id = sequence;
27
27
  items = [...items, {
28
28
  variant: "info",
29
- duration: 5000,
29
+ // Toast com ação não some antes de dar tempo de agir.
30
+ duration: options.actionLabel && options.onAction ? 0 : 5000,
30
31
  closable: true,
31
32
  ...options,
32
33
  id
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1doc/1ds-react",
3
- "version": "2.9.0",
3
+ "version": "2.11.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-core": "2.9.0",
49
- "@1doc/1ds-tokens": "0.5.0"
48
+ "@1doc/1ds-core": "2.11.0",
49
+ "@1doc/1ds-tokens": "0.6.0"
50
50
  },
51
51
  "publishConfig": {
52
52
  "access": "public"
@@ -0,0 +1,87 @@
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { useState } from "react";
3
+ import { fireEvent, render, screen } from "@testing-library/react";
4
+ import Accordion from "../components/Accordion/Accordion";
5
+
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: string) => screen.getByRole("button", { name: nome }).getAttribute("aria-expanded") === "true";
12
+
13
+ describe("Accordion · não controlado (como antes)", () => {
14
+ it("começa pelos defaultOpen e o clique abre e fecha; onOpenChange avisa a nova lista", () => {
15
+ const aoMudar = vi.fn();
16
+ render(<Accordion items={ITENS} multiple onOpenChange={aoMudar} />);
17
+ expect(aberto("Prazos")).toBe(true);
18
+ fireEvent.click(screen.getByRole("button", { name: "Anexos" }));
19
+ expect(aberto("Anexos")).toBe(true);
20
+ expect(aoMudar).toHaveBeenLastCalledWith(["prazos", "anexos"]);
21
+ fireEvent.click(screen.getByRole("button", { name: "Prazos" }));
22
+ expect(aberto("Prazos")).toBe(false);
23
+ expect(aoMudar).toHaveBeenLastCalledWith(["anexos"]);
24
+ });
25
+
26
+ it("sem multiple, abrir um fecha o outro", () => {
27
+ render(<Accordion items={ITENS} />);
28
+ fireEvent.click(screen.getByRole("button", { name: "Anexos" }));
29
+ expect(aberto("Anexos")).toBe(true);
30
+ expect(aberto("Prazos")).toBe(false);
31
+ });
32
+ });
33
+
34
+ describe("Accordion · controlado (open e onOpenChange)", () => {
35
+ it("open manda: o que está em open está aberto, ignorando defaultOpen", () => {
36
+ render(<Accordion items={ITENS} open={["sigilo"]} />);
37
+ expect(aberto("Sigilo")).toBe(true);
38
+ expect(aberto("Prazos")).toBe(false);
39
+ });
40
+
41
+ it("o clique só pede (onOpenChange); sem o app mudar open, nada abre", () => {
42
+ const aoMudar = vi.fn();
43
+ render(<Accordion items={ITENS} multiple open={["prazos"]} onOpenChange={aoMudar} />);
44
+ fireEvent.click(screen.getByRole("button", { name: "Anexos" }));
45
+ expect(aoMudar).toHaveBeenCalledWith(["prazos", "anexos"]);
46
+ expect(aberto("Anexos")).toBe(false);
47
+ });
48
+
49
+ it("o app guarda a escolha: com o estado dele, abre, fecha e a lista chega em ordem", () => {
50
+ let ultima: string[] = [];
51
+ function App() {
52
+ const [abertos, setAbertos] = useState<string[]>([]);
53
+ return (
54
+ <Accordion
55
+ items={ITENS}
56
+ multiple
57
+ open={abertos}
58
+ onOpenChange={(ids: string[]) => {
59
+ ultima = ids;
60
+ setAbertos(ids);
61
+ }}
62
+ />
63
+ );
64
+ }
65
+ render(<App />);
66
+ expect(aberto("Prazos")).toBe(false);
67
+ fireEvent.click(screen.getByRole("button", { name: "Sigilo" }));
68
+ fireEvent.click(screen.getByRole("button", { name: "Anexos" }));
69
+ expect(ultima).toEqual(["sigilo", "anexos"]);
70
+ expect(aberto("Sigilo") && aberto("Anexos")).toBe(true);
71
+ fireEvent.click(screen.getByRole("button", { name: "Sigilo" }));
72
+ expect(ultima).toEqual(["anexos"]);
73
+ expect(aberto("Sigilo")).toBe(false);
74
+ });
75
+
76
+ it("open vazio é controlado (tudo fechado), não ausência", () => {
77
+ render(<Accordion items={ITENS} open={[]} />);
78
+ expect(aberto("Prazos")).toBe(false);
79
+ });
80
+
81
+ it("item desabilitado não pede nada", () => {
82
+ const aoMudar = vi.fn();
83
+ render(<Accordion items={[{ id: "x", title: "Bloqueado", content: "...", disabled: true }]} open={[]} onOpenChange={aoMudar} />);
84
+ fireEvent.click(screen.getByRole("button", { name: "Bloqueado" }));
85
+ expect(aoMudar).not.toHaveBeenCalled();
86
+ });
87
+ });
@@ -0,0 +1,25 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import Accordion from "../components/Accordion/Accordion";
4
+
5
+ const ITENS = [
6
+ { id: "a", title: "Prazos", content: "Dez dias úteis." },
7
+ { id: "b", title: "Anexos", content: "Até 10 MB." },
8
+ ];
9
+
10
+ describe("Accordion · titleVariant", () => {
11
+ it("neutral liga a classe do título neutro; o padrão e default não", () => {
12
+ const { container, rerender } = render(<Accordion items={ITENS} titleVariant="neutral" />);
13
+ const raiz = () => container.firstElementChild as HTMLElement;
14
+ expect(raiz()).toHaveClass("umds-accordion--title-neutral");
15
+ rerender(<Accordion items={ITENS} titleVariant="default" />);
16
+ expect(raiz()).not.toHaveClass("umds-accordion--title-neutral");
17
+ rerender(<Accordion items={ITENS} />);
18
+ expect(raiz()).not.toHaveClass("umds-accordion--title-neutral");
19
+ });
20
+
21
+ it("combina com a variant do accordion", () => {
22
+ const { container } = render(<Accordion items={ITENS} variant="separated" titleVariant="neutral" />);
23
+ expect(container.firstElementChild).toHaveClass("umds-accordion--separated", "umds-accordion--title-neutral");
24
+ });
25
+ });
@@ -0,0 +1,17 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import AppSubheader from "../components/AppSubheader/AppSubheader";
4
+ import Breadcrumb from "../components/Breadcrumb/Breadcrumb";
5
+
6
+ /** O jsdom não tem layout: a geometria real é conferida pela story "Só a página atual" (play, no navegador); a regra de CSS, pelo teste do Vue. */
7
+ describe("AppSubheader · região do contexto", () => {
8
+ it("a trilha só com a página atual não tem nada focável dentro da região", () => {
9
+ const { container } = render(
10
+ <AppSubheader>
11
+ <Breadcrumb items={[{ label: "Painel" }]} />
12
+ </AppSubheader>,
13
+ );
14
+ const contexto = container.querySelector(".umds-app-subheader__contexto") as HTMLElement;
15
+ expect(contexto.querySelector("a, button, [tabindex]")).toBeNull();
16
+ });
17
+ });
@@ -0,0 +1,53 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
4
+ import AttachmentCard from "../components/AttachmentCard/AttachmentCard";
5
+
6
+ const DETALHES = {
7
+ description: "Documento legível.",
8
+ fields: [
9
+ { label: "Nome completo", value: "Marcos Vinícius Tavares", copyable: true },
10
+ { label: "CPF", value: "123.456.789-09" },
11
+ ],
12
+ };
13
+
14
+ async function abrirPainel() {
15
+ const usuario = userEvent.setup();
16
+ render(<AttachmentCard fileName="cnh.pdf" documentType="CNH" status="positive" statusLabel="Pré-aprovado" statusDetails={DETALHES} />);
17
+ await usuario.click(screen.getByRole("button", { name: /Pré-aprovado/ }));
18
+ return usuario;
19
+ }
20
+
21
+ describe("AttachmentCard · o <dl> do painel é válido (axe definition-list)", () => {
22
+ it("o dl só tem div filhos, e cada div só tem dt e dd, nessa ordem", async () => {
23
+ await abrirPainel();
24
+ const dl = document.querySelector(".umds-attachment-card__campos") as HTMLElement;
25
+ expect(dl.tagName).toBe("DL");
26
+ const grupos = [...dl.children];
27
+ expect(grupos).toHaveLength(2);
28
+ for (const grupo of grupos) {
29
+ expect(grupo.tagName).toBe("DIV");
30
+ expect([...grupo.children].map((f) => f.tagName)).toEqual(["DT", "DD"]);
31
+ }
32
+ });
33
+
34
+ it("o botão de copiar mora dentro do dd, ao lado do valor, e continua com nome", async () => {
35
+ await abrirPainel();
36
+ const dd = document.querySelector(".umds-attachment-card__campo-valor") as HTMLElement;
37
+ expect(dd.tagName).toBe("DD");
38
+ expect(dd.querySelector(".umds-attachment-card__campo-valor-texto")).toHaveTextContent("Marcos Vinícius Tavares");
39
+ const botao = dd.querySelector("button") as HTMLElement;
40
+ expect(botao).toHaveAttribute("aria-label", "Copiar Nome completo");
41
+ // nenhum botão solto direto num div do dl
42
+ for (const grupo of document.querySelectorAll(".umds-attachment-card__campo")) {
43
+ expect(grupo.querySelector(":scope > button")).toBeNull();
44
+ }
45
+ });
46
+
47
+ it("o campo sem copyable não tem botão e o valor segue no dd", async () => {
48
+ await abrirPainel();
49
+ const campos = document.querySelectorAll(".umds-attachment-card__campo");
50
+ expect(campos[1].querySelector("button")).toBeNull();
51
+ expect(campos[1].querySelector("dd")).toHaveTextContent("123.456.789-09");
52
+ });
53
+ });
@@ -0,0 +1,29 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import AttachmentCard from "../components/AttachmentCard/AttachmentCard";
4
+
5
+ const tamanho = (el: Element | null) => (el as HTMLElement).style.width;
6
+
7
+ describe("AttachmentCard · size", () => {
8
+ it("md (padrão): ícone do preview de 48px e o do rodapé no sm do Icon", () => {
9
+ const { container } = render(<AttachmentCard fileName="Protocolo 104-2026.pdf" />);
10
+ expect(container.firstElementChild).not.toHaveClass("umds-attachment-card--sm");
11
+ expect(tamanho(container.querySelector(".umds-attachment-card__preview .umds-icon"))).toBe("48px");
12
+ const rodape = container.querySelector(".umds-attachment-card__rodape-icone .umds-icon") as HTMLElement;
13
+ expect(rodape).toHaveClass("umds-icon--sm");
14
+ expect(rodape.style.width).toBe("");
15
+ });
16
+
17
+ it("sm: ícone do preview de 40px e o do rodapé de 12px", () => {
18
+ const { container } = render(<AttachmentCard fileName="Protocolo 104-2026.pdf" size="sm" />);
19
+ expect(container.firstElementChild).toHaveClass("umds-attachment-card--sm");
20
+ expect(tamanho(container.querySelector(".umds-attachment-card__preview .umds-icon"))).toBe("40px");
21
+ expect(tamanho(container.querySelector(".umds-attachment-card__rodape-icone .umds-icon"))).toBe("12px");
22
+ });
23
+
24
+ it("o ícone central escolhido continua valendo no sm", () => {
25
+ const { container } = render(<AttachmentCard fileName="a.pdf" size="sm" documentType="CNH" status="positive" statusLabel="Pré-aprovado" />);
26
+ expect(container.querySelector(".umds-attachment-card__tipo-texto")).toHaveTextContent("CNH");
27
+ expect(container.querySelector(".umds-attachment-card__status-texto")).toHaveTextContent("Pré-aprovado");
28
+ });
29
+ });
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import Badge from "../components/Badge/Badge";
4
+ import { lerArquivo } from "./lerArquivo";
5
+
6
+ const cssBadge = await lerArquivo("../core/src/components/Badge/Badge.css");
7
+
8
+ const css = cssBadge.replace(/\r\n/g, "\n");
9
+ const bloco = (seletor: string) => {
10
+ const inicio = css.indexOf(seletor + " {");
11
+ return css.slice(inicio, css.indexOf("}", inicio));
12
+ };
13
+
14
+ describe("Badge 2xs · padding lateral de 8px", () => {
15
+ it("a regra do 2xs usa o degrau nano (8px) dos lados e o micro (2px) em cima e embaixo", () => {
16
+ expect(bloco(".umds-badge--2xs")).toMatch(
17
+ /padding:\s*var\(--umds-primitive-spacing-micro\)\s+var\(--umds-primitive-spacing-nano\)/,
18
+ );
19
+ });
20
+
21
+ it("os outros tamanhos não mudam de padding", () => {
22
+ expect(bloco(".umds-badge--xs")).toMatch(/padding:\s*var\(--umds-primitive-spacing-micro\)\s+var\(--umds-primitive-spacing-quarck\)/);
23
+ expect(bloco(".umds-badge--md")).toMatch(/padding:\s*var\(--umds-primitive-spacing-quarck\)\s+var\(--umds-primitive-spacing-nano\)/);
24
+ });
25
+ });
26
+
27
+ describe("Badge attached · 22px e tom escuro", () => {
28
+ it("o rótulo colado tem altura mínima de 22px (24 - 2), de qualquer tamanho", () => {
29
+ expect(bloco(".umds-badge--attached")).toMatch(
30
+ /min-height:\s*calc\(var\(--umds-primitive-spacing-xxs\)\s*-\s*var\(--umds-primitive-spacing-micro\)\)/,
31
+ );
32
+ const { container } = render(<Badge attached size="xs" variant="neutral" appearance="solid">Tarefa manual</Badge>);
33
+ expect(container.firstElementChild).toHaveClass("umds-badge--attached", "umds-badge--xs");
34
+ });
35
+
36
+ it("attachedTone dark escreve a classe; sem ela e com light, não", () => {
37
+ const { container } = render(
38
+ <>
39
+ <Badge attached attachedTone="dark" variant="neutral" appearance="solid">A</Badge>
40
+ <Badge attached variant="neutral" appearance="solid">B</Badge>
41
+ <Badge attached attachedTone="light" variant="neutral" appearance="solid">C</Badge>
42
+ </>,
43
+ );
44
+ expect(container.children[0]).toHaveClass("umds-badge--attached-dark");
45
+ expect(container.children[1]).not.toHaveClass("umds-badge--attached-dark");
46
+ expect(container.children[2]).not.toHaveClass("umds-badge--attached-dark");
47
+ });
48
+
49
+ it("sem attached o tom é ignorado", () => {
50
+ const { container } = render(<Badge attachedTone="dark" variant="neutral" appearance="solid">A</Badge>);
51
+ expect(container.firstElementChild).not.toHaveClass("umds-badge--attached-dark");
52
+ });
53
+
54
+ it("CSS: o escuro é neutral-dark-down com texto branco, só no neutro sólido colado", () => {
55
+ const regra = bloco(".umds-badge--attached.umds-badge--neutral.umds-badge--solid.umds-badge--attached-dark");
56
+ expect(regra).toMatch(/background-color:\s*var\(--umds-semantic-color-neutral-dark-down\)/);
57
+ expect(regra).toMatch(/color:\s*var\(--umds-semantic-color-neutral-light-pure\)/);
58
+ expect(bloco(".umds-badge--attached.umds-badge--neutral.umds-badge--solid")).toMatch(/neutral-light-medium/);
59
+ });
60
+ });
@@ -0,0 +1,15 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import Badge from "../components/Badge/Badge";
4
+
5
+ /** A cascata de CSS de arquivo não existe no jsdom: a regra é conferida na fonte pelo teste do Vue; aqui, as classes no DOM. */
6
+ describe("Badge attached neutral", () => {
7
+ it("o neutro colado e sólido leva as três classes que a regra de CSS escolhe", () => {
8
+ const { container } = render(
9
+ <Badge attached variant="neutral" appearance="solid">
10
+ Tarefa manual
11
+ </Badge>,
12
+ );
13
+ expect(container.firstElementChild).toHaveClass("umds-badge--attached", "umds-badge--neutral", "umds-badge--solid");
14
+ });
15
+ });
@@ -62,3 +62,24 @@ describe("Tag compacta, soft, pílula e novidade", () => {
62
62
  expect(raiz(container)).toHaveClass("umds-tag--new", "umds-tag--soft");
63
63
  });
64
64
  });
65
+
66
+ describe("Badge attachedSide", () => {
67
+ it("right escreve o modificador do canto direito; left e o padrão não", () => {
68
+ const { container } = render(
69
+ <>
70
+ <Badge attached attachedSide="right">A</Badge>
71
+ <Badge attached attachedSide="left">B</Badge>
72
+ <Badge attached>C</Badge>
73
+ </>,
74
+ );
75
+ expect(container.children[0]).toHaveClass("umds-badge--attached", "umds-badge--attached-right");
76
+ expect(container.children[1]).not.toHaveClass("umds-badge--attached-right");
77
+ expect(container.children[2]).not.toHaveClass("umds-badge--attached-right");
78
+ });
79
+
80
+ it("attachedSide sem attached não faz nada", () => {
81
+ const { container } = render(<Badge attachedSide="right">A</Badge>);
82
+ expect(raiz(container)).not.toHaveClass("umds-badge--attached-right");
83
+ expect(raiz(container)).not.toHaveClass("umds-badge--attached");
84
+ });
85
+ });
@@ -0,0 +1,40 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import Button from "../components/Button/Button";
4
+ import { lerArquivo } from "./lerArquivo";
5
+
6
+ const cssButton = await lerArquivo("../core/src/components/Button/Button.css");
7
+
8
+ const css = cssButton.replace(/\r\n/g, "\n");
9
+ const bloco = (seletor: string) => {
10
+ const inicio = css.indexOf(seletor + " {");
11
+ return css.slice(inicio, css.indexOf("}", inicio));
12
+ };
13
+
14
+ describe("Button · density", () => {
15
+ it("comfortable escreve a classe; sem a prop e com default o botão é o de antes", () => {
16
+ render(
17
+ <>
18
+ <Button size="sm" density="comfortable">A</Button>
19
+ <Button size="sm">B</Button>
20
+ <Button size="sm" density="default">C</Button>
21
+ </>,
22
+ );
23
+ expect(screen.getByRole("button", { name: "A" })).toHaveClass("umds-button--sm", "umds-button--comfortable");
24
+ expect(screen.getByRole("button", { name: "B" })).not.toHaveClass("umds-button--comfortable");
25
+ expect(screen.getByRole("button", { name: "C" })).not.toHaveClass("umds-button--comfortable");
26
+ });
27
+
28
+ it("CSS: o sm confortável tem 12px de padding lateral; o sm de antes segue em 8px; o só-ícone fica de fora", () => {
29
+ expect(bloco(".umds-button--sm.umds-button--comfortable:not(.umds-button--icon)")).toMatch(
30
+ /padding:\s*0\s+var\(--umds-primitive-spacing-mini\)/,
31
+ );
32
+ expect(bloco(".umds-button--sm")).toMatch(/padding:\s*0\s+var\(--umds-primitive-spacing-nano\)/);
33
+ });
34
+
35
+ it("os outros tamanhos mantêm o padding (a densidade só mexe no sm)", () => {
36
+ expect(bloco(".umds-button--xs")).toMatch(/padding:\s*0\s+var\(--umds-primitive-spacing-mini\)/);
37
+ expect(bloco(".umds-button--md")).toMatch(/padding:\s*0\s+var\(--umds-primitive-spacing-xxxs\)/);
38
+ expect(bloco(".umds-button--lg")).toMatch(/padding:\s*0\s+var\(--umds-primitive-spacing-xxs\)/);
39
+ });
40
+ });
@@ -0,0 +1,11 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render, screen } from "@testing-library/react";
3
+ import Calendar from "../components/Calendar/Calendar";
4
+
5
+ describe("Calendar · cabeçalho", () => {
6
+ it("headerStart e headerEnd (React: props com nó) chegam ao cabeçalho", () => {
7
+ render(<Calendar events={[]} defaultDate="2026-10-08" headerStart={<span>Todos os documentos</span>} headerEnd={<span>Legenda</span>} />);
8
+ expect(screen.getByText("Todos os documentos")).toBeInTheDocument();
9
+ expect(screen.getByText("Legenda")).toBeInTheDocument();
10
+ });
11
+ });
@@ -0,0 +1,37 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import Calendar from "../components/Calendar/Calendar";
3
+ import { render } from "@testing-library/react";
4
+ import { lerArquivo } from "./lerArquivo";
5
+
6
+ const cssCalendar = await lerArquivo("../core/src/components/Calendar/Calendar.css");
7
+
8
+ const CSS: Record<string, string> = { Calendar: cssCalendar };
9
+ const css = (componente: string) => CSS[componente].replace(/\r\n/g, "\n");
10
+ const bloco = (texto: string, seletor: string) => {
11
+ const inicio = texto.indexOf(seletor + " {");
12
+ expect(inicio, seletor).toBeGreaterThanOrEqual(0);
13
+ return texto.slice(inicio, texto.indexOf("}", inicio));
14
+ };
15
+
16
+ describe("Calendar · density compact", () => {
17
+ it("compact escreve a classe na raiz; sem a prop, não", () => {
18
+ const { container } = render(
19
+ <>
20
+ <Calendar today="2026-05-15" defaultDate="2026-05-15" density="compact" />
21
+ <Calendar today="2026-05-15" defaultDate="2026-05-15" />
22
+ </>,
23
+ );
24
+ expect(container.children[0]).toHaveClass("umds-calendar--compact", "umds-calendar--month");
25
+ expect(container.children[1]).not.toHaveClass("umds-calendar--compact");
26
+ });
27
+
28
+ it("CSS: evento de 20px (16 + 4), 4px de padding lateral e linha de 16px; o título só é cortado quando passa da largura", () => {
29
+ const t = css("Calendar");
30
+ const compacto = bloco(t, ".umds-calendar--compact .umds-calendar__event");
31
+ expect(compacto).toMatch(/min-height:\s*calc\(var\(--umds-primitive-spacing-xxxs\)\s*\+\s*var\(--umds-primitive-spacing-quarck\)\)/);
32
+ expect(compacto).toMatch(/padding:\s*var\(--umds-primitive-spacing-micro\)\s+var\(--umds-primitive-spacing-quarck\)/);
33
+ expect(compacto).toMatch(/line-height:\s*var\(--umds-primitive-spacing-xxxs\)/);
34
+ const titulo = bloco(t, ".umds-calendar__event-title");
35
+ expect(titulo).toMatch(/text-overflow:\s*ellipsis/);
36
+ });
37
+ });