@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
@@ -0,0 +1,60 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { render } from "@testing-library/react";
3
+ import Tag from "../components/Tag/Tag";
4
+ import { lerArquivo } from "./lerArquivo";
5
+
6
+ const cssTag = await lerArquivo("../core/src/components/Tag/Tag.css");
7
+ const tema1doc = await lerArquivo("../tokens/dist/css/theme-1doc.css");
8
+ const temaEscuro = await lerArquivo("../tokens/dist/css/theme-1doc-dark.css");
9
+ const temaAltoContraste = await lerArquivo("../tokens/dist/css/theme-1doc-high-contrast.css");
10
+
11
+ const css = cssTag.replace(/\r\n/g, "\n");
12
+ const bloco = (seletor: string) => {
13
+ const inicio = css.indexOf(seletor + " {");
14
+ return css.slice(inicio, css.indexOf("}", inicio));
15
+ };
16
+
17
+ const TEMAS: Record<string, string> = { "theme-1doc.css": tema1doc, "theme-1doc-dark.css": temaEscuro, "theme-1doc-high-contrast.css": temaAltoContraste };
18
+ const tema = (arquivo: string) => {
19
+ const texto = TEMAS[arquivo];
20
+ const cor = (nome: string) => (texto.match(new RegExp("--umds-theme-" + nome + ":\\s*(#[0-9A-Fa-f]{6})")) as RegExpMatchArray)[1];
21
+ return cor;
22
+ };
23
+ const luz = (hex: string) => {
24
+ const c = [1, 3, 5].map((i) => parseInt(hex.slice(i, i + 2), 16) / 255).map((v) => (v <= 0.03928 ? v / 12.92 : ((v + 0.055) / 1.055) ** 2.4));
25
+ return 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
26
+ };
27
+ const contraste = (a: string, b: string) => {
28
+ const [x, y] = [luz(a), luz(b)].sort((m, n) => n - m);
29
+ return (x + 0.05) / (y + 0.05);
30
+ };
31
+
32
+ describe("Tag positive outlined sobre fundo tingido", () => {
33
+ it("escreve as classes que a regra de CSS escolhe", () => {
34
+ const { container } = render(<Tag label="Entregue" variant="positive" appearance="outlined" />);
35
+ const tag = container.firstElementChild as HTMLElement;
36
+ expect(tag).toHaveClass("umds-tag--outlined", "umds-tag--positive");
37
+ });
38
+
39
+ it("o texto é positive-dark e o contorno segue positive-pure", () => {
40
+ const regra = bloco(".umds-tag--outlined.umds-tag--positive");
41
+ expect(regra).toMatch(/color:\s*var\(--umds-semantic-color-feedback-positive-dark\)/);
42
+ expect(regra).toMatch(/border-color:\s*var\(--umds-semantic-color-feedback-positive-pure\)/);
43
+ });
44
+
45
+ it("no tema 1doc, sobre o azul-claro do card o texto passa de 4,5:1 (o pure dava 3,6:1)", () => {
46
+ const cor = tema("theme-1doc.css");
47
+ const fundo = cor("primary-light");
48
+ expect(contraste(cor("feedback-positive-pure"), fundo)).toBeLessThan(4.5);
49
+ expect(contraste(cor("feedback-positive-dark"), fundo)).toBeGreaterThanOrEqual(4.5);
50
+ });
51
+
52
+ it("nos temas escuro e de alto contraste o texto também passa de 4,5:1 sobre o fundo tingido e sobre a página", () => {
53
+ for (const arquivo of ["theme-1doc-dark.css", "theme-1doc-high-contrast.css"]) {
54
+ const cor = tema(arquivo);
55
+ for (const fundo of [cor("primary-light"), cor("background-white")]) {
56
+ expect(contraste(cor("feedback-positive-dark"), fundo)).toBeGreaterThanOrEqual(4.5);
57
+ }
58
+ }
59
+ });
60
+ });
@@ -0,0 +1,109 @@
1
+ import { afterEach, describe, expect, it, vi } from "vitest";
2
+ import { act, fireEvent, render, screen } from "@testing-library/react";
3
+ import Toast from "../components/Toast/Toast";
4
+ import ToastStack from "../components/Toast/ToastStack";
5
+ import { dismissAllToasts, toast } from "../utils/toast";
6
+
7
+ afterEach(() => {
8
+ act(() => dismissAllToasts());
9
+ });
10
+
11
+ describe("Toast · ação e detalhes", () => {
12
+ it("actionLabel com onAction desenha o botão; acionar chama onAction e fecha", () => {
13
+ const aoAgir = vi.fn();
14
+ const aoFechar = vi.fn();
15
+ render(<Toast open message="Despacho enviado" actionLabel="Desfazer" onAction={aoAgir} onClose={aoFechar} />);
16
+ fireEvent.click(screen.getByRole("button", { name: "Desfazer" }));
17
+ expect(aoAgir).toHaveBeenCalledTimes(1);
18
+ expect(aoFechar).toHaveBeenCalledTimes(1);
19
+ });
20
+
21
+ it("a ação roda antes de fechar", () => {
22
+ const ordem: string[] = [];
23
+ render(
24
+ <Toast
25
+ open
26
+ message="x"
27
+ actionLabel="Ver processo"
28
+ onAction={() => ordem.push("acao")}
29
+ onClose={() => ordem.push("fechar")}
30
+ />,
31
+ );
32
+ fireEvent.click(screen.getByRole("button", { name: "Ver processo" }));
33
+ expect(ordem).toEqual(["acao", "fechar"]);
34
+ });
35
+
36
+ it("actionLabel sem onAction, ou onAction sem actionLabel, não desenha botão de ação", () => {
37
+ const { rerender } = render(<Toast open message="x" actionLabel="Desfazer" />);
38
+ expect(screen.queryByRole("button", { name: "Desfazer" })).toBeNull();
39
+ rerender(<Toast open message="x" onAction={() => {}} />);
40
+ expect(document.querySelector(".umds-toast__action")).toBeNull();
41
+ });
42
+
43
+ it("details vira uma lista de linhas abaixo da descrição", () => {
44
+ render(<Toast open message="3 anexos não enviados" description="Corrija e envie de novo." details={["Anexo 1 acima de 10 MB", "Anexo 2 sem assunto"]} />);
45
+ const itens = screen.getAllByRole("listitem");
46
+ expect(itens.map((i) => i.textContent)).toEqual(["Anexo 1 acima de 10 MB", "Anexo 2 sem assunto"]);
47
+ });
48
+
49
+ it("sem details não há lista", () => {
50
+ render(<Toast open message="Salvo" />);
51
+ expect(document.querySelector(".umds-toast__details")).toBeNull();
52
+ });
53
+
54
+ it("o botão de fechar continua ao lado da ação", () => {
55
+ render(<Toast open message="x" actionLabel="Desfazer" onAction={() => {}} />);
56
+ expect(screen.getByRole("button", { name: "Fechar notificação" })).toBeInTheDocument();
57
+ });
58
+ });
59
+
60
+ describe("toast() imperativo com ação e detalhes", () => {
61
+ it("passa ação e detalhes ao ToastStack, e acionar a ação fecha o toast", () => {
62
+ const aoAgir = vi.fn();
63
+ render(<ToastStack />);
64
+ act(() => {
65
+ toast({ message: "Despacho enviado", details: ["SADM", "SOBR"], actionLabel: "Desfazer", onAction: aoAgir });
66
+ });
67
+ expect(screen.getByText("Despacho enviado")).toBeInTheDocument();
68
+ expect(screen.getAllByRole("listitem")).toHaveLength(2);
69
+ fireEvent.click(screen.getByRole("button", { name: "Desfazer" }));
70
+ expect(aoAgir).toHaveBeenCalledTimes(1);
71
+ expect(screen.queryByText("Despacho enviado")).toBeNull();
72
+ });
73
+
74
+ it("toast com ação não some sozinho (duration 0); sem ação continua em 5000", () => {
75
+ vi.useFakeTimers();
76
+ try {
77
+ render(<ToastStack />);
78
+ act(() => {
79
+ toast({ message: "Com ação", actionLabel: "Desfazer", onAction: () => {} });
80
+ toast({ message: "Sem ação" });
81
+ });
82
+ expect(screen.getByText("Com ação")).toBeInTheDocument();
83
+ expect(screen.getByText("Sem ação")).toBeInTheDocument();
84
+ act(() => {
85
+ vi.advanceTimersByTime(5100);
86
+ });
87
+ expect(screen.queryByText("Sem ação")).toBeNull();
88
+ expect(screen.getByText("Com ação")).toBeInTheDocument();
89
+ } finally {
90
+ vi.useRealTimers();
91
+ }
92
+ });
93
+
94
+ it("um duration explícito vale também com ação", () => {
95
+ vi.useFakeTimers();
96
+ try {
97
+ render(<ToastStack />);
98
+ act(() => {
99
+ toast({ message: "Curto", actionLabel: "Desfazer", onAction: () => {}, duration: 1000 });
100
+ });
101
+ act(() => {
102
+ vi.advanceTimersByTime(1100);
103
+ });
104
+ expect(screen.queryByText("Curto")).toBeNull();
105
+ } finally {
106
+ vi.useRealTimers();
107
+ }
108
+ });
109
+ });
@@ -0,0 +1,13 @@
1
+ /**
2
+ * Lê um arquivo do pacote (relativo à pasta `packages/react`) para os testes que conferem uma regra de CSS na
3
+ * fonte: o jsdom não tem cascata de arquivo, e o `css: false` do vitest esvazia o `?raw` do CSS.
4
+ * O `node:fs` entra por import dinâmico com nome calculado porque o `tsc` do pacote não traz os tipos do Node.
5
+ */
6
+ const modulo = (nome: string): Promise<any> => import(/* @vite-ignore */ nome);
7
+
8
+ export async function lerArquivo(relativo: string): Promise<string> {
9
+ const fs = await modulo("node:fs");
10
+ const path = await modulo("node:path");
11
+ const raiz: string = (globalThis as any).process.cwd();
12
+ return fs.readFileSync(path.resolve(raiz, relativo), "utf8");
13
+ }
@@ -5,14 +5,20 @@
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
17
 
15
- const PERMITIDAS = ["A", "B", "STRONG", "I", "EM", "U", "S", "STRIKE", "DEL", "UL", "OL", "LI", "P", "BR", "DIV", "SPAN", "H1", "H2", "H3", "H4", "BLOCKQUOTE"];
18
+ 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"];
19
+ /** Blocos que aceitam `text-align`. */
20
+ const ALINHAVEIS = ["P", "DIV", "H1", "H2", "H3", "H4", "LI", "BLOCKQUOTE", "TH", "TD"];
21
+ const ALINHAMENTOS = ["left", "center", "right", "justify"];
16
22
  const DESCARTADAS = ["SCRIPT", "STYLE", "IFRAME", "OBJECT", "EMBED", "LINK", "META", "TEMPLATE", "NOSCRIPT", "SVG", "MATH", "FORM", "INPUT", "BUTTON", "TEXTAREA", "SELECT"];
17
23
  const escapar = (texto: string): string => texto.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
18
24
 
@@ -41,6 +47,66 @@ export const normalizarEndereco = (bruto: string): string => {
41
47
  if (/^[a-z][a-z0-9+.-]*:/i.test(semControle)) return "";
42
48
  return "https://" + texto;
43
49
  };
50
+
51
+ /** Cor, tamanho e família de fonte só valem como TOKEN do DS: `var(--umds-...)`. Cor em hex, rgb ou nome sai. */
52
+ const COR_TOKEN = /^var\(--umds-(?:primitive|semantic)-[a-z0-9-]+\)$/;
53
+ const TAMANHO_TOKEN = /^var\(--umds-primitive-font-size-[a-z0-9]+\)$/;
54
+ const FAMILIA_TOKEN = /^var\(--umds-(?:primitive|semantic)-font-family-(?:base|mono)\)$/;
55
+
56
+ /**
57
+ * Declaração de estilo de texto que o editor aceita: `color`, `font-size` e `font-family`,
58
+ * cada uma só com um token do DS. Devolve a declaração limpa ("color: var(--x);") ou "".
59
+ */
60
+ export const estiloDeTexto = (propriedade: string, valor: string): string => {
61
+ const prop = String(propriedade || "").trim().toLowerCase();
62
+ const val = String(valor || "").trim();
63
+ if (prop === "color" && COR_TOKEN.test(val)) return "color: " + val + ";";
64
+ if (prop === "font-size" && TAMANHO_TOKEN.test(val)) return "font-size: " + val + ";";
65
+ if (prop === "font-family" && FAMILIA_TOKEN.test(val)) return "font-family: " + val + ";";
66
+ return "";
67
+ };
68
+
69
+ /** As declarações válidas de um atributo `style` (só color, font-size e font-family por token). */
70
+ const estiloDoSpan = (estilo: string): string => {
71
+ const partes = String(estilo || "").split(";");
72
+ let saida = "";
73
+ for (let i = 0; i < partes.length; i++) {
74
+ const corte = partes[i].indexOf(":");
75
+ if (corte === -1) continue;
76
+ const decl = estiloDeTexto(partes[i].slice(0, corte), partes[i].slice(corte + 1));
77
+ if (decl) saida += (saida ? " " : "") + decl;
78
+ }
79
+ return saida;
80
+ };
81
+
82
+ /**
83
+ * Endereço de imagem que o editor aceita: `http(s)`, caminho (`/x`, `./x`, `../x`) ou
84
+ * `data:image` em base64 de PNG, JPEG, GIF ou WebP (SVG fica de fora: carrega script).
85
+ * Devolve o endereço limpo, ou "" se for perigoso ou vazio.
86
+ */
87
+ export const imagemSegura = (bruto: string): string => {
88
+ const texto = (bruto || "").trim();
89
+ if (!texto) return "";
90
+ const semControle = texto.replace(/[\u0000-\u001f\u007f\s]+/g, "");
91
+ if (/^data:image\/(png|jpe?g|gif|webp);base64,[a-z0-9+/]+=*$/i.test(semControle)) return semControle;
92
+ // `//host/x` (sem esquema) carregaria de outro site sem o autor ter pedido: fica de fora.
93
+ if (/^(https?:\/\/|\/(?!\/)|\.\.?\/)/i.test(semControle)) return texto;
94
+ return "";
95
+ };
96
+
97
+ /** O `text-align` de um elemento, pelo `style` ou pelo atributo `align`: left, center, right, justify ou "". */
98
+ const alinhamentoDe = (elemento: any): string => {
99
+ const estilo = String(elemento.getAttribute("style") || "");
100
+ const achado = /(?:^|;)\s*text-align\s*:\s*([a-z]+)\s*(?:;|$)/i.exec(estilo);
101
+ const valor = (achado ? achado[1] : String(elemento.getAttribute("align") || "")).toLowerCase();
102
+ return ALINHAMENTOS.indexOf(valor) !== -1 ? valor : "";
103
+ };
104
+
105
+ /** Número inteiro de 2 a 99 em `colspan`/`rowspan`; o resto some. */
106
+ const vao = (valor: any): string => {
107
+ const n = parseInt(String(valor || ""), 10);
108
+ return n >= 2 && n <= 99 ? String(n) : "";
109
+ };
44
110
  const limparNo = (no: any, saida: any, documento: any): void => {
45
111
  const filhos = Array.prototype.slice.call(no.childNodes);
46
112
  for (let i = 0; i < filhos.length; i++) {
@@ -57,7 +123,31 @@ const limparNo = (no: any, saida: any, documento: any): void => {
57
123
  limparNo(filho, saida, documento);
58
124
  continue;
59
125
  }
126
+ if (tag === "IMG") {
127
+ // Imagem sem endereço seguro some inteira (não deixa um <img> quebrado nem carregando de fora).
128
+ const origem = imagemSegura(filho.getAttribute("src") || "");
129
+ if (!origem) continue;
130
+ const imagem = documento.createElement("img");
131
+ imagem.setAttribute("src", origem);
132
+ imagem.setAttribute("alt", String(filho.getAttribute("alt") || ""));
133
+ saida.appendChild(imagem);
134
+ continue;
135
+ }
60
136
  const novo = documento.createElement(tag.toLowerCase());
137
+ if (ALINHAVEIS.indexOf(tag) !== -1) {
138
+ const alinhamento = alinhamentoDe(filho);
139
+ if (alinhamento) novo.setAttribute("style", "text-align: " + alinhamento + ";");
140
+ }
141
+ if (tag === "SPAN") {
142
+ const estiloValido = estiloDoSpan(filho.getAttribute("style") || "");
143
+ if (estiloValido) novo.setAttribute("style", estiloValido);
144
+ }
145
+ if (tag === "TD" || tag === "TH") {
146
+ const colunas = vao(filho.getAttribute("colspan"));
147
+ const linhas = vao(filho.getAttribute("rowspan"));
148
+ if (colunas) novo.setAttribute("colspan", colunas);
149
+ if (linhas) novo.setAttribute("rowspan", linhas);
150
+ }
61
151
  if (tag === "A") {
62
152
  const href = enderecoSeguro(filho.getAttribute("href") || "");
63
153
  if (href) {
@@ -81,8 +171,21 @@ export const sanitizarHtml = (html: string): string => {
81
171
  return saida.innerHTML;
82
172
  };
83
173
 
174
+ /** A tabela vazia que o editor insere: `linhas` × `colunas` células com uma quebra, para o cursor entrar. */
175
+ export const htmlDaTabela = (linhas: number, colunas: number): string => {
176
+ let celulas = "";
177
+ for (let c = 0; c < colunas; c++) celulas += "<td><br></td>";
178
+ let corpo = "";
179
+ for (let l = 0; l < linhas; l++) corpo += "<tr>" + celulas + "</tr>";
180
+ return "<table><tbody>" + corpo + "</tbody></table><p><br></p>";
181
+ };
182
+
183
+ /** O `<img>` que o editor insere (endereço já seguro). */
184
+ export const htmlDaImagem = (endereco: string, alt: string): string => '<img src="' + escapar(endereco) + '" alt="' + escapar(alt || "") + '">';
185
+
84
186
  /** O editor está "vazio": sem texto, sem lista e sem quebra solta (o `<br>` que o navegador deixa). */
85
187
  export const htmlVazio = (html: string): boolean => {
188
+ if (/<(img|table)[\s>]/i.test(html || "")) return false;
86
189
  const texto = (html || "").replace(/<br\s*\/?>/gi, "").replace(/<\/?(div|p|span)[^>]*>/gi, "").trim();
87
190
  return texto === "";
88
191
  };
@@ -22,7 +22,13 @@ export interface ToastOptions {
22
22
  description?: string;
23
23
  /** Variante semântica @default 'info' */
24
24
  variant?: ToastVariant;
25
- /** Auto-dismiss em ms; 0 = não fecha sozinho @default 5000 */
25
+ /** Linhas de detalhe abaixo da descrição */
26
+ details?: string[];
27
+ /** Rótulo do botão de ação ("Desfazer"); só aparece com `onAction` */
28
+ actionLabel?: string;
29
+ /** Chamado ao acionar o botão de ação; depois dele o toast fecha */
30
+ onAction?: (event?: any) => void;
31
+ /** Auto-dismiss em ms; 0 = não fecha sozinho @default 5000 (0 quando há ação: dá tempo de agir, WCAG 2.2.1) */
26
32
  duration?: number;
27
33
  /** Exibe o botão de fechar @default true */
28
34
  closable?: boolean;
@@ -31,6 +37,9 @@ export interface ToastData {
31
37
  id: number;
32
38
  message: string;
33
39
  description?: string;
40
+ details?: string[];
41
+ actionLabel?: string;
42
+ onAction?: (event?: any) => void;
34
43
  variant: ToastVariant;
35
44
  duration: number;
36
45
  closable: boolean;
@@ -50,7 +59,8 @@ export const toast = (options: ToastOptions): number => {
50
59
  const id = sequence;
51
60
  items = [...items, {
52
61
  variant: "info",
53
- duration: 5000,
62
+ // Toast com ação não some antes de dar tempo de agir.
63
+ duration: options.actionLabel && options.onAction ? 0 : 5000,
54
64
  closable: true,
55
65
  ...options,
56
66
  id