@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.
- package/CHANGELOG.md +653 -0
- package/dist/components/Accordion/Accordion.css +13 -0
- package/dist/components/Accordion/Accordion.d.ts +9 -0
- package/dist/components/Accordion/Accordion.js +18 -5
- package/dist/components/AppSubheader/AppSubheader.css +10 -1
- package/dist/components/AttachmentCard/AttachmentCard.css +10 -8
- package/dist/components/AttachmentCard/AttachmentCard.d.ts +3 -0
- package/dist/components/AttachmentCard/AttachmentCard.js +8 -1
- package/dist/components/Badge/Badge.css +27 -5
- package/dist/components/Badge/Badge.d.ts +20 -4
- package/dist/components/Badge/Badge.js +6 -0
- package/dist/components/Button/Button.css +5 -0
- package/dist/components/Button/Button.d.ts +3 -0
- package/dist/components/Button/Button.js +1 -0
- package/dist/components/Calendar/Calendar.css +10 -0
- package/dist/components/Calendar/Calendar.d.ts +10 -6
- package/dist/components/Calendar/Calendar.js +6 -1
- package/dist/components/Coachmark/Coachmark.css +10 -0
- package/dist/components/Coachmark/Coachmark.d.ts +14 -1
- package/dist/components/Coachmark/Coachmark.js +51 -5
- package/dist/components/ColorPicker/ColorPicker.css +8 -1
- package/dist/components/ColorPicker/ColorPicker.d.ts +2 -2
- package/dist/components/DataTable/DataTable.css +216 -0
- package/dist/components/DataTable/DataTable.d.ts +70 -2
- package/dist/components/DataTable/DataTable.js +293 -6
- package/dist/components/DescriptionList/DescriptionList.css +14 -0
- package/dist/components/DescriptionList/DescriptionList.d.ts +3 -0
- package/dist/components/DescriptionList/DescriptionList.js +1 -0
- package/dist/components/EmptyState/EmptyState.d.ts +4 -2
- package/dist/components/EmptyState/EmptyState.js +2 -2
- package/dist/components/Input/Input.css +30 -0
- package/dist/components/Input/Input.d.ts +10 -0
- package/dist/components/Input/Input.js +3 -1
- package/dist/components/List/ListItem.d.ts +14 -2
- package/dist/components/List/ListItem.js +3 -2
- package/dist/components/Modal/Modal.css +19 -0
- package/dist/components/Modal/Modal.d.ts +7 -0
- package/dist/components/Modal/Modal.js +1 -0
- package/dist/components/RichTextEditor/RichTextEditor.css +47 -0
- package/dist/components/RichTextEditor/RichTextEditor.d.ts +22 -8
- package/dist/components/RichTextEditor/RichTextEditor.js +460 -3
- package/dist/components/SegmentedControl/SegmentedControl.css +70 -0
- package/dist/components/SegmentedControl/SegmentedControl.d.ts +5 -0
- package/dist/components/SegmentedControl/SegmentedControl.js +5 -1
- package/dist/components/Select/Select.css +122 -2
- package/dist/components/Select/Select.d.ts +33 -4
- package/dist/components/Select/Select.js +256 -35
- package/dist/components/StatCard/StatCard.css +73 -0
- package/dist/components/StatCard/StatCard.d.ts +12 -0
- package/dist/components/StatCard/StatCard.js +11 -1
- package/dist/components/Tabs/Tabs.css +11 -0
- package/dist/components/Tabs/Tabs.d.ts +7 -0
- package/dist/components/Tabs/Tabs.js +7 -1
- package/dist/components/Tag/Tag.css +3 -1
- package/dist/components/Toast/Toast.css +32 -0
- package/dist/components/Toast/Toast.d.ts +18 -0
- package/dist/components/Toast/Toast.js +14 -1
- package/dist/components/Toast/ToastStack.js +8 -1
- package/dist/teste/AccordionControlado.test.d.ts +1 -0
- package/dist/teste/AccordionControlado.test.js +73 -0
- package/dist/teste/AccordionTituloNeutro.test.d.ts +1 -0
- package/dist/teste/AccordionTituloNeutro.test.js +23 -0
- package/dist/teste/AppSubheaderRegiao.test.d.ts +1 -0
- package/dist/teste/AppSubheaderRegiao.test.js +13 -0
- package/dist/teste/AttachmentCardLista.test.d.ts +1 -0
- package/dist/teste/AttachmentCardLista.test.js +49 -0
- package/dist/teste/AttachmentCardSm.test.d.ts +1 -0
- package/dist/teste/AttachmentCardSm.test.js +26 -0
- package/dist/teste/BadgeAttachedTomEPadding.test.d.ts +1 -0
- package/dist/teste/BadgeAttachedTomEPadding.test.js +43 -0
- package/dist/teste/BadgeNeutroColado.test.d.ts +1 -0
- package/dist/teste/BadgeNeutroColado.test.js +11 -0
- package/dist/teste/BadgeTag.test.js +13 -0
- package/dist/teste/ButtonDensidade.test.d.ts +1 -0
- package/dist/teste/ButtonDensidade.test.js +28 -0
- package/dist/teste/CalendarCabecalho.test.d.ts +1 -0
- package/dist/teste/CalendarCabecalho.test.js +11 -0
- package/dist/teste/CalendarCompacto.test.d.ts +1 -0
- package/dist/teste/CalendarCompacto.test.js +29 -0
- package/dist/teste/CoachmarkSemVeu.test.d.ts +1 -0
- package/dist/teste/CoachmarkSemVeu.test.js +85 -0
- package/dist/teste/CoachmarkUmPasso.test.d.ts +1 -0
- package/dist/teste/CoachmarkUmPasso.test.js +34 -0
- package/dist/teste/ColorPickerXs.test.d.ts +1 -0
- package/dist/teste/ColorPickerXs.test.js +21 -0
- package/dist/teste/DataTableArvoreEVariantes.test.d.ts +1 -0
- package/dist/teste/DataTableArvoreEVariantes.test.js +190 -0
- package/dist/teste/DataTableCompactaETom.test.d.ts +1 -0
- package/dist/teste/DataTableCompactaETom.test.js +69 -0
- package/dist/teste/DescriptionListCompacta.test.d.ts +1 -0
- package/dist/teste/DescriptionListCompacta.test.js +22 -0
- package/dist/teste/EmptyStateTituloRico.test.d.ts +1 -0
- package/dist/teste/EmptyStateTituloRico.test.js +23 -0
- package/dist/teste/InputAgrupado.test.d.ts +1 -0
- package/dist/teste/InputAgrupado.test.js +24 -0
- package/dist/teste/InputGhost.test.d.ts +1 -0
- package/dist/teste/InputGhost.test.js +32 -0
- package/dist/teste/ListItemTooltipStandalone.test.d.ts +1 -0
- package/dist/teste/ListItemTooltipStandalone.test.js +39 -0
- package/dist/teste/ModalProporcoes1Doc.test.d.ts +1 -0
- package/dist/teste/ModalProporcoes1Doc.test.js +32 -0
- package/dist/teste/RichTextEditorAlinhaImagemTabela.test.d.ts +1 -0
- package/dist/teste/RichTextEditorAlinhaImagemTabela.test.js +212 -0
- package/dist/teste/RichTextEditorEstiloDeTexto.test.d.ts +1 -0
- package/dist/teste/RichTextEditorEstiloDeTexto.test.js +120 -0
- package/dist/teste/SegmentedControlContador.test.d.ts +1 -0
- package/dist/teste/SegmentedControlContador.test.js +39 -0
- package/dist/teste/SegmentedControlSuperficie.test.d.ts +1 -0
- package/dist/teste/SegmentedControlSuperficie.test.js +39 -0
- package/dist/teste/SelectAbertoControladoEFlutuante.test.d.ts +1 -0
- package/dist/teste/SelectAbertoControladoEFlutuante.test.js +145 -0
- package/dist/teste/SelectFiltroCorETamanho.test.d.ts +1 -0
- package/dist/teste/SelectFiltroCorETamanho.test.js +77 -0
- package/dist/teste/SelectItemComEscopo.test.d.ts +1 -0
- package/dist/teste/SelectItemComEscopo.test.js +54 -0
- package/dist/teste/SelectListaFiltro.test.d.ts +1 -0
- package/dist/teste/SelectListaFiltro.test.js +102 -0
- package/dist/teste/StatCardSecundariaETom.test.d.ts +1 -0
- package/dist/teste/StatCardSecundariaETom.test.js +45 -0
- package/dist/teste/StatCardTamanhoSm.test.d.ts +1 -0
- package/dist/teste/StatCardTamanhoSm.test.js +32 -0
- package/dist/teste/TabsCompacta.test.d.ts +1 -0
- package/dist/teste/TabsCompacta.test.js +35 -0
- package/dist/teste/TagPositivoTingido.test.d.ts +1 -0
- package/dist/teste/TagPositivoTingido.test.js +54 -0
- package/dist/teste/ToastAcaoEDetalhes.test.d.ts +1 -0
- package/dist/teste/ToastAcaoEDetalhes.test.js +94 -0
- package/dist/teste/lerArquivo.d.ts +1 -0
- package/dist/teste/lerArquivo.js +12 -0
- package/dist/utils/html-seguro.d.ts +19 -1
- package/dist/utils/html-seguro.js +113 -2
- package/dist/utils/toast.d.ts +10 -1
- package/dist/utils/toast.js +2 -1
- package/package.json +3 -3
- package/src/teste/AccordionControlado.test.tsx +87 -0
- package/src/teste/AccordionTituloNeutro.test.tsx +25 -0
- package/src/teste/AppSubheaderRegiao.test.tsx +17 -0
- package/src/teste/AttachmentCardLista.test.tsx +53 -0
- package/src/teste/AttachmentCardSm.test.tsx +29 -0
- package/src/teste/BadgeAttachedTomEPadding.test.tsx +60 -0
- package/src/teste/BadgeNeutroColado.test.tsx +15 -0
- package/src/teste/BadgeTag.test.tsx +21 -0
- package/src/teste/ButtonDensidade.test.tsx +40 -0
- package/src/teste/CalendarCabecalho.test.tsx +11 -0
- package/src/teste/CalendarCompacto.test.tsx +37 -0
- package/src/teste/CoachmarkSemVeu.test.tsx +101 -0
- package/src/teste/CoachmarkUmPasso.test.tsx +45 -0
- package/src/teste/ColorPickerXs.test.tsx +22 -0
- package/src/teste/DataTableArvoreEVariantes.test.tsx +240 -0
- package/src/teste/DataTableCompactaETom.test.tsx +85 -0
- package/src/teste/DescriptionListCompacta.test.tsx +24 -0
- package/src/teste/EmptyStateTituloRico.test.tsx +34 -0
- package/src/teste/InputAgrupado.test.tsx +26 -0
- package/src/teste/InputGhost.test.tsx +42 -0
- package/src/teste/ListItemTooltipStandalone.test.tsx +64 -0
- package/src/teste/ModalProporcoes1Doc.test.tsx +36 -0
- package/src/teste/RichTextEditorAlinhaImagemTabela.test.tsx +238 -0
- package/src/teste/RichTextEditorEstiloDeTexto.test.tsx +137 -0
- package/src/teste/SegmentedControlContador.test.tsx +43 -0
- package/src/teste/SegmentedControlSuperficie.test.tsx +52 -0
- package/src/teste/SelectAbertoControladoEFlutuante.test.tsx +180 -0
- package/src/teste/SelectFiltroCorETamanho.test.tsx +90 -0
- package/src/teste/SelectItemComEscopo.test.tsx +69 -0
- package/src/teste/SelectListaFiltro.test.tsx +120 -0
- package/src/teste/StatCardSecundariaETom.test.tsx +51 -0
- package/src/teste/StatCardTamanhoSm.test.tsx +42 -0
- package/src/teste/TabsCompacta.test.tsx +45 -0
- package/src/teste/TagPositivoTingido.test.tsx +60 -0
- package/src/teste/ToastAcaoEDetalhes.test.tsx +109 -0
- package/src/teste/lerArquivo.ts +13 -0
- package/src/utils/html-seguro.ts +105 -2
- 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
|
+
}
|
package/src/utils/html-seguro.ts
CHANGED
|
@@ -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
|
|
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, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """);
|
|
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
|
};
|
package/src/utils/toast.ts
CHANGED
|
@@ -22,7 +22,13 @@ export interface ToastOptions {
|
|
|
22
22
|
description?: string;
|
|
23
23
|
/** Variante semântica @default 'info' */
|
|
24
24
|
variant?: ToastVariant;
|
|
25
|
-
/**
|
|
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
|
-
|
|
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
|