@1doc/1ds-react 2.11.0 → 2.12.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 CHANGED
@@ -1,5 +1,27 @@
1
1
  # @1doc/1ds-react
2
2
 
3
+ ## 2.12.0
4
+
5
+ ### Minor Changes
6
+
7
+ - d03c66d: Timeline: estado vazio dentro do componente, como o List. `isEmpty`, `emptyTitle`, `emptyDescription`, `emptyIcon`, `emptySize`, `emptyActionLabel` com `onEmptyAction` e o slot `empty` (no React, a prop `empty`) trocam só a trilha por um `EmptyState` com `role="status"`, sem desmontar o que está em volta. Sem `isEmpty`, `items` vazio mostra o vazio quando há `emptyTitle` ou o slot; sem nenhum dos dois, a trilha vazia segue como antes. No Vue, `isEmpty` ausente agora chega como `undefined` (e não `false`), para distinguir "não passou" de "passou false".
8
+
9
+ ## 2.11.1
10
+
11
+ ### Patch Changes
12
+
13
+ - ffc6176: remove(tokens): `--umds-semantic-font-family-mono` e `--umds-primitive-font-family-mono` saem
14
+
15
+ REMOÇÃO de token, não acréscimo. O DS não cria token novo: a família monoespaçada (introduzida no 0.6.0) volta a não
16
+ existir. Como estamos em 0.x, a remoção sai como minor nos tokens.
17
+
18
+ - `Kbd` e `CodeBlock` passam a usar `--umds-semantic-font-family-base`: o texto da tecla e do código sai na fonte do DS,
19
+ não mais monoespaçada (muda a largura dos caracteres; o espaço e as quebras de linha do `CodeBlock` seguem mantidos).
20
+ - `RichTextEditor`: a opção "Monoespaçada" da ferramenta `fontFamily` sai (o label `fontFamilyMono` também). Fica só a
21
+ opção "Padrão" (`fontFamilyBase`). A higienização do valor passa a aceitar só `font-family-base` em `font-family`:
22
+ um `<span style="font-family: var(--umds-semantic-font-family-mono)">` salvo antes perde a declaração ao carregar.
23
+ - Quem usou o token: troque por `--umds-semantic-font-family-base`.
24
+
3
25
  ## 2.11.0
4
26
 
5
27
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  /* ─────────────────────────────────────────────
2
2
  CODE BLOCK
3
- Texto em fonte monoespaçada, com borda, rolagem e botão de copiar.
3
+ Texto na fonte do DS (a base, sem fonte de código), com borda, rolagem e botão de copiar.
4
4
 
5
5
  Fundo claro e texto escuro (contraste de texto, nos temas claro e escuro: os
6
6
  dois vêm dos semânticos). Sem cor de sintaxe.
@@ -28,7 +28,7 @@
28
28
  }
29
29
 
30
30
  .umds-code-block__language {
31
- font-family: var(--umds-semantic-font-family-mono);
31
+ font-family: var(--umds-semantic-font-family-base);
32
32
  font-size: var(--umds-primitive-font-size-xxxs); /* 12px */
33
33
  font-weight: var(--umds-primitive-font-weight-semibold);
34
34
  color: var(--umds-semantic-color-neutral-dark-down);
@@ -65,7 +65,7 @@
65
65
  margin: 0;
66
66
  padding: var(--umds-primitive-spacing-mini) var(--umds-primitive-spacing-xxxs); /* 12px 16px */
67
67
  overflow: auto;
68
- font-family: var(--umds-semantic-font-family-mono);
68
+ font-family: var(--umds-semantic-font-family-base);
69
69
  font-size: var(--umds-primitive-font-size-xxs); /* 14px */
70
70
  line-height: var(--umds-primitive-font-line-height-lg);
71
71
  color: var(--umds-semantic-color-neutral-dark-pure);
@@ -1,6 +1,6 @@
1
1
  import './CodeBlock.css';
2
2
  /**
3
- * CodeBlock — trecho de código (ou JSON, ou um log) em fonte monoespaçada, com
3
+ * CodeBlock — trecho de código (ou JSON, ou um log) com a fonte do DS, com
4
4
  * borda, rolagem e botão de copiar.
5
5
  *
6
6
  * O texto vem por `code` ou como conteúdo (`<CodeBlock>{...}</CodeBlock>`); o
@@ -25,7 +25,7 @@ import './CodeBlock.css';
25
25
  * @property {string} ariaLabel - Nome da área de rolagem (padrão: a linguagem, ou "Código")
26
26
  * @property {string} className - Classes CSS adicionais
27
27
  * @property {function} onCopy - Chamado com o texto depois de copiar
28
- * @quandoUsar Mostrar JSON de uma resposta, um comando, um log, um exemplo de uso: texto que se lê em fonte monoespaçada e se copia.
28
+ * @quandoUsar Mostrar JSON de uma resposta, um comando, um log, um exemplo de uso: texto que se lê como foi escrito e se copia.
29
29
  * @quandoNaoUsar Para uma tecla ou atalho, use `Kbd`. Para código dentro de uma frase, use `<code>` direto. Para editar código, use um editor especializado: este bloco só mostra.
30
30
  */
31
31
  export interface CodeBlockProps {
@@ -1,6 +1,6 @@
1
1
  /* ─────────────────────────────────────────────
2
2
  KBD
3
- Uma tecla, ou uma combinação, em fonte monoespaçada.
3
+ Uma tecla, ou uma combinação, na fonte do DS (a base).
4
4
 
5
5
  A caixa é cinza com borda e uma sombra de base de 1px (a "espessura" da
6
6
  tecla). Texto escuro sobre cinza claro: contraste de texto, não só de cor.
@@ -13,7 +13,7 @@
13
13
  box-sizing: border-box;
14
14
  min-width: var(--umds-primitive-spacing-xxs); /* 24px */
15
15
  padding: var(--umds-primitive-spacing-micro) var(--umds-primitive-spacing-nano); /* 2px 8px */
16
- font-family: var(--umds-semantic-font-family-mono);
16
+ font-family: var(--umds-semantic-font-family-base);
17
17
  font-weight: var(--umds-primitive-font-weight-semibold);
18
18
  line-height: var(--umds-primitive-font-line-height-lg);
19
19
  text-align: center;
@@ -40,7 +40,7 @@
40
40
  display: inline-flex;
41
41
  align-items: center;
42
42
  gap: var(--umds-primitive-spacing-quarck); /* 4px */
43
- font-family: var(--umds-semantic-font-family-mono);
43
+ font-family: var(--umds-semantic-font-family-base);
44
44
  }
45
45
 
46
46
  .umds-kbd-combo__item {
@@ -4,7 +4,7 @@ import './Kbd.css';
4
4
  *
5
5
  * Uma tecla: `<Kbd>Esc</Kbd>`. Uma combinação: `keys` (`["Ctrl", "Shift", "P"]`),
6
6
  * que desenha uma caixa por tecla com o `separator` entre elas (`+` por padrão).
7
- * A fonte é a monoespaçada do DS (`--umds-semantic-font-family-mono`), a caixa é
7
+ * A fonte é a do DS (`--umds-semantic-font-family-base`), a caixa é
8
8
  * cinza com borda e uma sombra de base, como uma tecla de verdade.
9
9
  *
10
10
  * Semântica: `<kbd>` é o elemento HTML de "entrada de teclado", e um `<kbd>`
@@ -35,7 +35,7 @@ export type RichTextTool = "bold" | "italic" | "underline" | "strike" | "ul" | "
35
35
  * higienização: só PNG, JPEG, GIF e WebP em `data:`, ou `http(s)`; sem SVG.
36
36
  *
37
37
  * Estilo de texto por TOKEN do DS, também opcional na `toolbar`: `fontFamily`
38
- * (padrão ou monoespaçada), `fontSize` (12, 14, 16, 20 e 24px, da escala do DS) e
38
+ * (a fonte do DS), `fontSize` (12, 14, 16, 20 e 24px, da escala do DS) e
39
39
  * `textColor` (a paleta do DS). Cada um abre um painel abaixo da barra e aplica ao
40
40
  * texto selecionado (sem seleção, o painel pede que se selecione). O valor guarda
41
41
  * `<span style="color: var(--umds-primitive-red-pure)">`: só token do DS passa na
@@ -53,7 +53,7 @@ export type RichTextTool = "bold" | "italic" | "underline" | "strike" | "ul" | "
53
53
  * @property {string} placeholder - Texto de apoio com o editor vazio
54
54
  * @property {string[]} toolbar - Ferramentas da barra, em ordem: 'bold' | 'italic' | 'underline' | 'strike' | 'ul' | 'ol' | 'link' | 'undo' | 'redo' | 'clear' | 'alignLeft' | 'alignCenter' | 'alignRight' | 'alignJustify' | 'image' | 'table' | 'fontFamily' | 'fontSize' | 'textColor' | '|' (separador)
55
55
  * @property {string} toolbarLabel - Nome da barra (padrão "Formatação")
56
- * @property {object} labels - Nomes das ferramentas e dos campos, parcial: `{ bold, italic, underline, strike, ul, ol, link, undo, redo, clear, alignLeft, alignCenter, alignRight, alignJustify, image, table, linkAddress, linkApply, linkRemove, linkCancel, linkInvalid, imageUrl, imageAlt, imageFile, imageApply, imageCancel, imageInvalid, imageType, imageTooLarge, tableRows, tableCols, tableApply, tableCancel, tableInvalid, tableActions, tableAddRow, tableRemoveRow, tableAddCol, tableRemoveCol, tableRemove, fontFamily, fontSize, textColor, fontFamilyBase, fontFamilyMono, styleClose, styleNeedsSelection }`
56
+ * @property {object} labels - Nomes das ferramentas e dos campos, parcial: `{ bold, italic, underline, strike, ul, ol, link, undo, redo, clear, alignLeft, alignCenter, alignRight, alignJustify, image, table, linkAddress, linkApply, linkRemove, linkCancel, linkInvalid, imageUrl, imageAlt, imageFile, imageApply, imageCancel, imageInvalid, imageType, imageTooLarge, tableRows, tableCols, tableApply, tableCancel, tableInvalid, tableActions, tableAddRow, tableRemoveRow, tableAddCol, tableRemoveCol, tableRemove, fontFamily, fontSize, textColor, fontFamilyBase, styleClose, styleNeedsSelection }`
57
57
  * @property {number} minHeight - Altura mínima da área de escrita, em px (padrão 120)
58
58
  * @property {boolean} resizable - Deixa redimensionar a altura pelo canto
59
59
  * @property {boolean} disabled - Desabilita barra e área
@@ -148,7 +148,6 @@ function RichTextEditor(props) {
148
148
  fontSize: "Tamanho da fonte",
149
149
  textColor: "Cor do texto",
150
150
  fontFamilyBase: "Padrão",
151
- fontFamilyMono: "Monoespaçada",
152
151
  styleClose: "Fechar",
153
152
  styleNeedsSelection: "Selecione o texto antes de aplicar.",
154
153
  };
@@ -790,10 +789,6 @@ function RichTextEditor(props) {
790
789
  label: rotulo("fontFamilyBase"),
791
790
  value: "var(--umds-semantic-font-family-base)",
792
791
  },
793
- {
794
- label: rotulo("fontFamilyMono"),
795
- value: "var(--umds-semantic-font-family-mono)",
796
- },
797
792
  ];
798
793
  }
799
794
  function fontSizeOptions() {
@@ -214,3 +214,11 @@
214
214
  .umds-timeline--sm.umds-timeline--card .umds-timeline__body {
215
215
  padding: var(--umds-primitive-spacing-mini); /* 12px */
216
216
  }
217
+
218
+ /* ── Estado vazio ──
219
+ O miolo da trilha troca de conteúdo; quem rodeia não muda. O EmptyState já
220
+ tem o próprio respiro e alinhamento, o wrapper só dá o lugar. */
221
+ .umds-timeline-empty {
222
+ display: block;
223
+ font-family: var(--umds-semantic-font-family-base);
224
+ }
@@ -35,20 +35,56 @@ export interface TimelineItem {
35
35
  * Extraída do histórico de versões do processo e da timeline de despachos da
36
36
  * 1Doc.
37
37
  *
38
+ * Sem eventos, a trilha troca por um estado vazio dentro do próprio componente:
39
+ * um `EmptyState` (`emptyTitle`, `emptyDescription`, `emptyIcon`,
40
+ * `emptyActionLabel` com `onEmptyAction`) ou, para um conteúdo próprio, o slot
41
+ * `empty` (no React, a prop `empty` recebe o nó). Só o miolo troca: o título do
42
+ * cartão, os filtros e o que mais estiver ao redor continuam montados, e o
43
+ * vazio é anunciado ao leitor de tela (`role="status"`). Passe `isEmpty` quando
44
+ * a decisão for sua (`:is-empty="!eventos.length"`); sem ele, `items` vazio já
45
+ * mostra o vazio, desde que haja `emptyTitle` ou o slot. Sem nenhum dos dois,
46
+ * `items` vazio rende a trilha sem eventos, como sempre.
47
+ *
38
48
  * @property {TimelineItem[]} items - Eventos, na ordem em que devem aparecer.
39
49
  * @property {boolean} card - Envolve o conteúdo de cada evento num card branco bordered.
40
50
  * @property {string} size - Tamanho do marcador e do texto: 'sm' | 'md'.
41
51
  * @property {string} ariaLabel - Nome acessível da lista. Padrão: "Linha do tempo".
52
+ * @property {boolean} isEmpty - Decide o estado vazio: `true` mostra o vazio no lugar da trilha, `false` mantém a trilha. Sem ele, o vazio aparece quando `items` está vazio e há `emptyTitle` ou o slot `empty`.
53
+ * @property {string} emptyTitle - Título do estado vazio (padrão: "Nenhum evento registrado")
54
+ * @property {string} emptyDescription - Texto de apoio do estado vazio
55
+ * @property {IconProp} emptyIcon - Ícone do estado vazio (objeto do Font Awesome)
56
+ * @property {string} emptySize - Tamanho do estado vazio: 'sm' (padrão, compacto) | 'md' | 'lg'
57
+ * @property {string} emptyActionLabel - Rótulo do botão do estado vazio (junto de onEmptyAction)
58
+ * @property {function} onEmptyAction - Callback do botão do estado vazio
59
+ * @property {any} empty - Slot `empty`: conteúdo próprio no lugar do EmptyState (React node ou Vue slot)
42
60
  * @property {string} className - Classes CSS adicionais.
43
- * @quandoUsar Mostrar o que aconteceu e quando — auditoria, histórico de versões, despachos de um processo.
44
- * @quandoNaoUsar Para um processo com etapas futuras e progresso, use `Stepper`. Para uma lista sem eixo de tempo, use `List` ou `Card`.
61
+ * @slot empty - Conteúdo do estado vazio (substitui o EmptyState padrão).
62
+ * @quandoUsar Mostrar o que aconteceu e quando — auditoria, histórico de versões, despachos de um processo — inclusive quando ainda não aconteceu nada: o "nenhum despacho registrado" fica no componente, sem `v-if` por fora.
63
+ * @quandoNaoUsar Para um processo com etapas futuras e progresso, use `Stepper`. Para uma lista sem eixo de tempo, use `List` ou `Card`. Para um vazio que ocupa a tela inteira (sem trilha), use `EmptyState` direto.
45
64
  */
46
65
  export interface TimelineProps {
47
66
  items: TimelineItem[];
48
67
  card?: boolean;
49
68
  size?: "sm" | "md";
50
69
  ariaLabel?: string;
70
+ /** Decide o estado vazio. Sem ele, vale `items` vazio com `emptyTitle` ou slot. */
71
+ isEmpty?: boolean;
72
+ /** Título do estado vazio. @default 'Nenhum evento registrado' */
73
+ emptyTitle?: string;
74
+ /** Texto de apoio do estado vazio. */
75
+ emptyDescription?: string;
76
+ /** Ícone do estado vazio: o objeto importado do Font Awesome. */
77
+ emptyIcon?: IconProp;
78
+ /** Tamanho do estado vazio. @default 'sm' */
79
+ emptySize?: "sm" | "md" | "lg";
80
+ /** Rótulo do botão do estado vazio (exibido junto de `onEmptyAction`). */
81
+ emptyActionLabel?: string;
82
+ /** Chamado no botão do estado vazio. */
83
+ onEmptyAction?: () => void;
84
+ /** Conteúdo próprio do estado vazio. No Vue, slot `empty`. */
85
+ empty?: any;
51
86
  className?: string;
52
87
  }
88
+ import type { IconProp } from "../Icon/Icon";
53
89
  declare function Timeline(props: TimelineProps): import("react/jsx-runtime").JSX.Element;
54
90
  export default Timeline;
@@ -1,8 +1,36 @@
1
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import './Timeline.css';
3
3
  import Text from "../Text/Text";
4
4
  import Icon from "../Icon/Icon";
5
+ import EmptyState from "../EmptyState/EmptyState";
5
6
  function Timeline(props) {
7
+ function showEmpty() {
8
+ if (props.isEmpty !== undefined && props.isEmpty !== null) {
9
+ return !!props.isEmpty;
10
+ }
11
+ const semEventos = !props.items || props.items.length === 0;
12
+ return semEventos && !!(props.emptyTitle || props.empty);
13
+ }
14
+ function showItems() {
15
+ return !showEmpty();
16
+ }
17
+ function classesDoVazio() {
18
+ return ["umds-timeline-empty", props.className].filter(Boolean).join(" ");
19
+ }
20
+ function emptyTitleText() {
21
+ return props.emptyTitle || "Nenhum evento registrado";
22
+ }
23
+ function emptyActionProps() {
24
+ if (!props.onEmptyAction)
25
+ return {};
26
+ return {
27
+ actionLabel: props.emptyActionLabel,
28
+ onAction: props.onEmptyAction,
29
+ };
30
+ }
31
+ function emptySizeValue() {
32
+ return props.emptySize || "sm";
33
+ }
6
34
  function classesDaLista() {
7
35
  return [
8
36
  "umds-timeline",
@@ -36,6 +64,6 @@ function Timeline(props) {
36
64
  function temCabecalho(item) {
37
65
  return !!(item.title || item.timestamp || item.actor);
38
66
  }
39
- return (_jsx("ol", { role: "list", "aria-label": props.ariaLabel || "Linha do tempo", className: classesDaLista(), children: props.items?.map((item, index) => (_jsxs("li", { "aria-current": item.current ? "true" : undefined, className: classesDoItem(item), children: [_jsxs("div", { "aria-hidden": "true", className: "umds-timeline__rail", children: [_jsx("span", { className: classesDoMarcador(item), children: item.icon ? _jsx(Icon, { size: "sm", icon: item.icon }) : null }), !ehUltimo(index) ? (_jsx("span", { className: "umds-timeline__connector" })) : null] }), _jsxs("div", { className: "umds-timeline__body", children: [temCabecalho(item) ? (_jsxs("div", { className: "umds-timeline__header", children: [item.title ? (_jsx(Text, { as: "span", variant: "body2", weight: "semibold", children: item.title })) : null, item.timestamp ? (_jsx(Text, { as: "span", variant: "caption", color: "secondary", children: item.timestamp })) : null, item.actor ? (_jsx(Text, { as: "span", variant: "caption", color: "secondary", children: item.actor })) : null] })) : null, item.description ? (_jsx(Text, { as: "p", variant: "body2", color: "secondary", children: item.description })) : null] })] }, index))) }));
67
+ return (_jsxs(_Fragment, { children: [showEmpty() ? (_jsx("div", { role: "status", className: classesDoVazio(), children: _jsx(_Fragment, { children: props.empty || (_jsx(EmptyState, { icon: props.emptyIcon, title: emptyTitleText(), description: props.emptyDescription, size: emptySizeValue(), ...emptyActionProps() })) }) })) : null, showItems() ? (_jsx("ol", { role: "list", "aria-label": props.ariaLabel || "Linha do tempo", className: classesDaLista(), children: props.items?.map((item, index) => (_jsxs("li", { "aria-current": item.current ? "true" : undefined, className: classesDoItem(item), children: [_jsxs("div", { "aria-hidden": "true", className: "umds-timeline__rail", children: [_jsx("span", { className: classesDoMarcador(item), children: item.icon ? _jsx(Icon, { size: "sm", icon: item.icon }) : null }), !ehUltimo(index) ? (_jsx("span", { className: "umds-timeline__connector" })) : null] }), _jsxs("div", { className: "umds-timeline__body", children: [temCabecalho(item) ? (_jsxs("div", { className: "umds-timeline__header", children: [item.title ? (_jsx(Text, { as: "span", variant: "body2", weight: "semibold", children: item.title })) : null, item.timestamp ? (_jsx(Text, { as: "span", variant: "caption", color: "secondary", children: item.timestamp })) : null, item.actor ? (_jsx(Text, { as: "span", variant: "caption", color: "secondary", children: item.actor })) : null] })) : null, item.description ? (_jsx(Text, { as: "p", variant: "body2", color: "secondary", children: item.description })) : null] })] }, index))) })) : null] }));
40
68
  }
41
69
  export default Timeline;
@@ -25,7 +25,7 @@ describe("html-seguro: estilo de texto por token", () => {
25
25
  expect(estiloDeTexto("color", "var(--umds-primitive-red-pure)")).toBe("color: var(--umds-primitive-red-pure);");
26
26
  expect(estiloDeTexto("color", "var(--umds-semantic-color-feedback-negative-pure)")).toBe("color: var(--umds-semantic-color-feedback-negative-pure);");
27
27
  expect(estiloDeTexto("font-size", "var(--umds-primitive-font-size-xs)")).toBe("font-size: var(--umds-primitive-font-size-xs);");
28
- expect(estiloDeTexto("font-family", "var(--umds-semantic-font-family-mono)")).toBe("font-family: var(--umds-semantic-font-family-mono);");
28
+ expect(estiloDeTexto("font-family", "var(--umds-semantic-font-family-base)")).toBe("font-family: var(--umds-semantic-font-family-base);");
29
29
  for (const mau of ["red", "#ff0000", "rgb(255,0,0)", "var(--outra-coisa)", "var(--umds-primitive-red-pure); background: red", "expression(alert(1))", "url(x)"]) {
30
30
  expect(estiloDeTexto("color", mau)).toBe("");
31
31
  }
@@ -36,7 +36,7 @@ describe("html-seguro: estilo de texto por token", () => {
36
36
  });
37
37
  it("o span guarda só as declarações válidas; o resto do style sai", () => {
38
38
  expect(sanitizarHtml('<span style="color: var(--umds-primitive-red-pure); background: red; font-size: 40px">a</span>')).toBe('<span style="color: var(--umds-primitive-red-pure);">a</span>');
39
- expect(sanitizarHtml('<span style="font-family: var(--umds-semantic-font-family-mono); font-size: var(--umds-primitive-font-size-sm)">a</span>')).toBe('<span style="font-family: var(--umds-semantic-font-family-mono); font-size: var(--umds-primitive-font-size-sm);">a</span>');
39
+ expect(sanitizarHtml('<span style="font-family: var(--umds-semantic-font-family-base); font-size: var(--umds-primitive-font-size-sm)">a</span>')).toBe('<span style="font-family: var(--umds-semantic-font-family-base); font-size: var(--umds-primitive-font-size-sm);">a</span>');
40
40
  expect(sanitizarHtml('<span style="color: red">a</span>')).toBe("<span>a</span>");
41
41
  });
42
42
  it("estilo só vale em span: em outras tags sai", () => {
@@ -62,15 +62,15 @@ describe("RichTextEditor: fonte, tamanho e cor do texto", () => {
62
62
  fireEvent.keyDown(screen.getByRole("group", { name: "Cor do texto" }), { key: "Escape" });
63
63
  expect(screen.queryByRole("group", { name: "Cor do texto" })).toBeNull();
64
64
  });
65
- it("a fonte monoespaçada embrulha o texto selecionado num span com o token", async () => {
65
+ it("a fonte do DS embrulha o texto selecionado num span com o token", async () => {
66
66
  const usuario = userEvent.setup();
67
67
  const aoMudar = vi.fn();
68
68
  abrir(["fontFamily"], { onChange: aoMudar });
69
69
  selecionarTexto();
70
70
  await usuario.click(screen.getByRole("button", { name: "Fonte" }));
71
71
  await usuario.click(screen.getByRole("combobox", { name: "Fonte" }));
72
- await usuario.click(screen.getByRole("option", { name: "Monoespaçada" }));
73
- expect(execCommand).toHaveBeenCalledWith("insertHTML", false, '<span style="font-family: var(--umds-semantic-font-family-mono);">Parecer</span>');
72
+ await usuario.click(screen.getByRole("option", { name: "Padrão" }));
73
+ expect(execCommand).toHaveBeenCalledWith("insertHTML", false, '<span style="font-family: var(--umds-semantic-font-family-base);">Parecer</span>');
74
74
  expect(aoMudar).toHaveBeenCalled();
75
75
  expect(screen.queryByRole("group", { name: "Fonte" })).toBeNull();
76
76
  });
@@ -105,11 +105,11 @@ describe("RichTextEditor: fonte, tamanho e cor do texto", () => {
105
105
  });
106
106
  it("os textos vêm por labels", async () => {
107
107
  const usuario = userEvent.setup();
108
- abrir(["fontFamily", "textColor"], { labels: { fontFamily: "Font", textColor: "Text color", fontFamilyMono: "Monospaced", styleClose: "Close" } });
108
+ abrir(["fontFamily", "textColor"], { labels: { fontFamily: "Font", textColor: "Text color", fontFamilyBase: "Default", styleClose: "Close" } });
109
109
  selecionarTexto();
110
110
  await usuario.click(screen.getByRole("button", { name: "Font" }));
111
111
  await usuario.click(screen.getByRole("combobox", { name: "Font" }));
112
- expect(screen.getByRole("option", { name: "Monospaced" })).toBeInTheDocument();
112
+ expect(screen.getByRole("option", { name: "Default" })).toBeInTheDocument();
113
113
  await usuario.keyboard("{Escape}");
114
114
  expect(screen.getByRole("button", { name: "Close" })).toBeInTheDocument();
115
115
  });
@@ -1,6 +1,7 @@
1
- import { jsx as _jsx } from "react/jsx-runtime";
2
- import { describe, expect, it } from "vitest";
3
- import { render, screen } from "@testing-library/react";
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it, vi } from "vitest";
3
+ import { render, screen, waitFor } from "@testing-library/react";
4
+ import userEvent from "@testing-library/user-event";
4
5
  import { faCheck } from "@fortawesome/free-solid-svg-icons";
5
6
  import Timeline from "../components/Timeline/Timeline";
6
7
  /**
@@ -71,3 +72,72 @@ describe("Timeline · conteúdo", () => {
71
72
  expect(screen.getByRole("list", { name: "Despachos do processo" })).toBeInTheDocument();
72
73
  });
73
74
  });
75
+ describe("Timeline · estado vazio", () => {
76
+ it("isEmpty troca a trilha pelo vazio, com role status e o título padrão", () => {
77
+ const { container } = render(_jsx(Timeline, { items: EVENTOS, isEmpty: true }));
78
+ expect(screen.getByRole("status")).toBeInTheDocument();
79
+ expect(screen.getByText("Nenhum evento registrado")).toBeInTheDocument();
80
+ expect(screen.queryByRole("list")).toBeNull();
81
+ expect(screen.queryAllByRole("listitem")).toHaveLength(0);
82
+ expect(container.querySelector(".umds-timeline__marker")).toBeNull();
83
+ });
84
+ it("título, descrição e ícone do vazio vêm das props", () => {
85
+ const { container } = render(_jsx(Timeline, { items: [], isEmpty: true, emptyTitle: "Nenhum despacho registrado", emptyDescription: "O hist\u00F3rico aparece aqui.", emptyIcon: faCheck }));
86
+ expect(screen.getByText("Nenhum despacho registrado")).toBeInTheDocument();
87
+ expect(screen.getByText("O histórico aparece aqui.")).toBeInTheDocument();
88
+ expect(container.querySelector(".umds-empty-state svg")).toBeInTheDocument();
89
+ });
90
+ it("sem isEmpty, items vazio com emptyTitle mostra o vazio", () => {
91
+ render(_jsx(Timeline, { items: [], emptyTitle: "Nada por aqui" }));
92
+ expect(screen.getByRole("status")).toBeInTheDocument();
93
+ expect(screen.getByText("Nada por aqui")).toBeInTheDocument();
94
+ expect(screen.queryByRole("list")).toBeNull();
95
+ });
96
+ it("sem isEmpty, items vazio com a prop empty (slot) mostra o conteúdo próprio", () => {
97
+ render(_jsx(Timeline, { items: [], empty: _jsx("span", { "data-testid": "proprio", children: "Sem despachos" }) }));
98
+ expect(screen.getByRole("status")).toContainElement(screen.getByTestId("proprio"));
99
+ // o EmptyState padrão não entra: o conteúdo próprio o substitui
100
+ expect(screen.queryByText("Nenhum evento registrado")).toBeNull();
101
+ });
102
+ it("sem isEmpty e sem emptyTitle nem slot, items vazio segue sendo a trilha vazia", () => {
103
+ render(_jsx(Timeline, { items: [] }));
104
+ expect(screen.queryByRole("status")).toBeNull();
105
+ expect(screen.getByRole("list")).toBeInTheDocument();
106
+ });
107
+ it("isEmpty false manda: items vazio com emptyTitle mantém a trilha", () => {
108
+ render(_jsx(Timeline, { items: [], isEmpty: false, emptyTitle: "Nada por aqui" }));
109
+ expect(screen.queryByRole("status")).toBeNull();
110
+ expect(screen.getByRole("list")).toBeInTheDocument();
111
+ });
112
+ it("items com eventos e emptyTitle, sem isEmpty, mostra a trilha", () => {
113
+ render(_jsx(Timeline, { items: EVENTOS, emptyTitle: "Nada por aqui" }));
114
+ expect(screen.queryByRole("status")).toBeNull();
115
+ expect(screen.getAllByRole("listitem")).toHaveLength(3);
116
+ });
117
+ it("o botão do vazio só aparece com rótulo e callback, e chama o callback", async () => {
118
+ const usuario = userEvent.setup();
119
+ const aoAgir = vi.fn();
120
+ const { rerender } = render(_jsx(Timeline, { items: [], isEmpty: true, emptyActionLabel: "Limpar filtro" }));
121
+ expect(screen.queryByRole("button")).toBeNull();
122
+ rerender(_jsx(Timeline, { items: [], isEmpty: true, emptyActionLabel: "Limpar filtro", onEmptyAction: aoAgir }));
123
+ await usuario.click(screen.getByRole("button", { name: "Limpar filtro" }));
124
+ expect(aoAgir).toHaveBeenCalledTimes(1);
125
+ });
126
+ it("o entorno não desmonta: só o miolo troca ao esvaziar e ao voltar", async () => {
127
+ const { rerender } = render(_jsxs("section", { children: [_jsx("h3", { "data-testid": "titulo", children: "Hist\u00F3rico" }), _jsx(Timeline, { items: EVENTOS, isEmpty: false, emptyTitle: "Sem eventos" })] }));
128
+ const titulo = screen.getByTestId("titulo");
129
+ expect(screen.getAllByRole("listitem")).toHaveLength(3);
130
+ rerender(_jsxs("section", { children: [_jsx("h3", { "data-testid": "titulo", children: "Hist\u00F3rico" }), _jsx(Timeline, { items: [], isEmpty: true, emptyTitle: "Sem eventos" })] }));
131
+ await waitFor(() => expect(screen.getByRole("status")).toBeInTheDocument());
132
+ expect(screen.getByTestId("titulo")).toBe(titulo);
133
+ expect(screen.queryByRole("list")).toBeNull();
134
+ rerender(_jsxs("section", { children: [_jsx("h3", { "data-testid": "titulo", children: "Hist\u00F3rico" }), _jsx(Timeline, { items: EVENTOS, isEmpty: false, emptyTitle: "Sem eventos" })] }));
135
+ await waitFor(() => expect(screen.getAllByRole("listitem")).toHaveLength(3));
136
+ expect(screen.getByTestId("titulo")).toBe(titulo);
137
+ expect(screen.queryByRole("status")).toBeNull();
138
+ });
139
+ it("className vai ao wrapper do vazio", () => {
140
+ render(_jsx(Timeline, { items: [], isEmpty: true, className: "meu-vazio" }));
141
+ expect(screen.getByRole("status")).toHaveClass("meu-vazio");
142
+ });
143
+ });
@@ -53,7 +53,7 @@ export const normalizarEndereco = (bruto) => {
53
53
  /** Cor, tamanho e família de fonte só valem como TOKEN do DS: `var(--umds-...)`. Cor em hex, rgb ou nome sai. */
54
54
  const COR_TOKEN = /^var\(--umds-(?:primitive|semantic)-[a-z0-9-]+\)$/;
55
55
  const TAMANHO_TOKEN = /^var\(--umds-primitive-font-size-[a-z0-9]+\)$/;
56
- const FAMILIA_TOKEN = /^var\(--umds-(?:primitive|semantic)-font-family-(?:base|mono)\)$/;
56
+ const FAMILIA_TOKEN = /^var\(--umds-(?:primitive|semantic)-font-family-base\)$/;
57
57
  /**
58
58
  * Declaração de estilo de texto que o editor aceita: `color`, `font-size` e `font-family`,
59
59
  * cada uma só com um token do DS. Devolve a declaração limpa ("color: var(--x);") ou "".
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1doc/1ds-react",
3
- "version": "2.11.0",
3
+ "version": "2.12.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.11.0",
49
- "@1doc/1ds-tokens": "0.6.0"
48
+ "@1doc/1ds-core": "2.12.0",
49
+ "@1doc/1ds-tokens": "0.7.0"
50
50
  },
51
51
  "publishConfig": {
52
52
  "access": "public"
@@ -29,7 +29,7 @@ describe("html-seguro: estilo de texto por token", () => {
29
29
  expect(estiloDeTexto("color", "var(--umds-primitive-red-pure)")).toBe("color: var(--umds-primitive-red-pure);");
30
30
  expect(estiloDeTexto("color", "var(--umds-semantic-color-feedback-negative-pure)")).toBe("color: var(--umds-semantic-color-feedback-negative-pure);");
31
31
  expect(estiloDeTexto("font-size", "var(--umds-primitive-font-size-xs)")).toBe("font-size: var(--umds-primitive-font-size-xs);");
32
- expect(estiloDeTexto("font-family", "var(--umds-semantic-font-family-mono)")).toBe("font-family: var(--umds-semantic-font-family-mono);");
32
+ expect(estiloDeTexto("font-family", "var(--umds-semantic-font-family-base)")).toBe("font-family: var(--umds-semantic-font-family-base);");
33
33
  for (const mau of ["red", "#ff0000", "rgb(255,0,0)", "var(--outra-coisa)", "var(--umds-primitive-red-pure); background: red", "expression(alert(1))", "url(x)"]) {
34
34
  expect(estiloDeTexto("color", mau)).toBe("");
35
35
  }
@@ -43,8 +43,8 @@ describe("html-seguro: estilo de texto por token", () => {
43
43
  expect(sanitizarHtml('<span style="color: var(--umds-primitive-red-pure); background: red; font-size: 40px">a</span>')).toBe(
44
44
  '<span style="color: var(--umds-primitive-red-pure);">a</span>',
45
45
  );
46
- expect(sanitizarHtml('<span style="font-family: var(--umds-semantic-font-family-mono); font-size: var(--umds-primitive-font-size-sm)">a</span>')).toBe(
47
- '<span style="font-family: var(--umds-semantic-font-family-mono); font-size: var(--umds-primitive-font-size-sm);">a</span>',
46
+ expect(sanitizarHtml('<span style="font-family: var(--umds-semantic-font-family-base); font-size: var(--umds-primitive-font-size-sm)">a</span>')).toBe(
47
+ '<span style="font-family: var(--umds-semantic-font-family-base); font-size: var(--umds-primitive-font-size-sm);">a</span>',
48
48
  );
49
49
  expect(sanitizarHtml('<span style="color: red">a</span>')).toBe("<span>a</span>");
50
50
  });
@@ -74,15 +74,15 @@ describe("RichTextEditor: fonte, tamanho e cor do texto", () => {
74
74
  expect(screen.queryByRole("group", { name: "Cor do texto" })).toBeNull();
75
75
  });
76
76
 
77
- it("a fonte monoespaçada embrulha o texto selecionado num span com o token", async () => {
77
+ it("a fonte do DS embrulha o texto selecionado num span com o token", async () => {
78
78
  const usuario = userEvent.setup();
79
79
  const aoMudar = vi.fn();
80
80
  abrir(["fontFamily"], { onChange: aoMudar });
81
81
  selecionarTexto();
82
82
  await usuario.click(screen.getByRole("button", { name: "Fonte" }));
83
83
  await usuario.click(screen.getByRole("combobox", { name: "Fonte" }));
84
- await usuario.click(screen.getByRole("option", { name: "Monoespaçada" }));
85
- expect(execCommand).toHaveBeenCalledWith("insertHTML", false, '<span style="font-family: var(--umds-semantic-font-family-mono);">Parecer</span>');
84
+ await usuario.click(screen.getByRole("option", { name: "Padrão" }));
85
+ expect(execCommand).toHaveBeenCalledWith("insertHTML", false, '<span style="font-family: var(--umds-semantic-font-family-base);">Parecer</span>');
86
86
  expect(aoMudar).toHaveBeenCalled();
87
87
  expect(screen.queryByRole("group", { name: "Fonte" })).toBeNull();
88
88
  });
@@ -121,11 +121,11 @@ describe("RichTextEditor: fonte, tamanho e cor do texto", () => {
121
121
 
122
122
  it("os textos vêm por labels", async () => {
123
123
  const usuario = userEvent.setup();
124
- abrir(["fontFamily", "textColor"], { labels: { fontFamily: "Font", textColor: "Text color", fontFamilyMono: "Monospaced", styleClose: "Close" } });
124
+ abrir(["fontFamily", "textColor"], { labels: { fontFamily: "Font", textColor: "Text color", fontFamilyBase: "Default", styleClose: "Close" } });
125
125
  selecionarTexto();
126
126
  await usuario.click(screen.getByRole("button", { name: "Font" }));
127
127
  await usuario.click(screen.getByRole("combobox", { name: "Font" }));
128
- expect(screen.getByRole("option", { name: "Monospaced" })).toBeInTheDocument();
128
+ expect(screen.getByRole("option", { name: "Default" })).toBeInTheDocument();
129
129
  await usuario.keyboard("{Escape}");
130
130
  expect(screen.getByRole("button", { name: "Close" })).toBeInTheDocument();
131
131
  });
@@ -1,5 +1,6 @@
1
- import { describe, expect, it } from "vitest";
2
- import { render, screen } from "@testing-library/react";
1
+ import { describe, expect, it, vi } from "vitest";
2
+ import { render, screen, waitFor } from "@testing-library/react";
3
+ import userEvent from "@testing-library/user-event";
3
4
  import { faCheck } from "@fortawesome/free-solid-svg-icons";
4
5
  import Timeline from "../components/Timeline/Timeline";
5
6
 
@@ -98,3 +99,120 @@ describe("Timeline · conteúdo", () => {
98
99
  ).toBeInTheDocument();
99
100
  });
100
101
  });
102
+
103
+ describe("Timeline · estado vazio", () => {
104
+ it("isEmpty troca a trilha pelo vazio, com role status e o título padrão", () => {
105
+ const { container } = render(<Timeline items={EVENTOS} isEmpty />);
106
+ expect(screen.getByRole("status")).toBeInTheDocument();
107
+ expect(screen.getByText("Nenhum evento registrado")).toBeInTheDocument();
108
+ expect(screen.queryByRole("list")).toBeNull();
109
+ expect(screen.queryAllByRole("listitem")).toHaveLength(0);
110
+ expect(container.querySelector(".umds-timeline__marker")).toBeNull();
111
+ });
112
+
113
+ it("título, descrição e ícone do vazio vêm das props", () => {
114
+ const { container } = render(
115
+ <Timeline
116
+ items={[]}
117
+ isEmpty
118
+ emptyTitle="Nenhum despacho registrado"
119
+ emptyDescription="O histórico aparece aqui."
120
+ emptyIcon={faCheck}
121
+ />,
122
+ );
123
+ expect(screen.getByText("Nenhum despacho registrado")).toBeInTheDocument();
124
+ expect(screen.getByText("O histórico aparece aqui.")).toBeInTheDocument();
125
+ expect(container.querySelector(".umds-empty-state svg")).toBeInTheDocument();
126
+ });
127
+
128
+ it("sem isEmpty, items vazio com emptyTitle mostra o vazio", () => {
129
+ render(<Timeline items={[]} emptyTitle="Nada por aqui" />);
130
+ expect(screen.getByRole("status")).toBeInTheDocument();
131
+ expect(screen.getByText("Nada por aqui")).toBeInTheDocument();
132
+ expect(screen.queryByRole("list")).toBeNull();
133
+ });
134
+
135
+ it("sem isEmpty, items vazio com a prop empty (slot) mostra o conteúdo próprio", () => {
136
+ render(
137
+ <Timeline items={[]} empty={<span data-testid="proprio">Sem despachos</span>} />,
138
+ );
139
+ expect(screen.getByRole("status")).toContainElement(
140
+ screen.getByTestId("proprio"),
141
+ );
142
+ // o EmptyState padrão não entra: o conteúdo próprio o substitui
143
+ expect(screen.queryByText("Nenhum evento registrado")).toBeNull();
144
+ });
145
+
146
+ it("sem isEmpty e sem emptyTitle nem slot, items vazio segue sendo a trilha vazia", () => {
147
+ render(<Timeline items={[]} />);
148
+ expect(screen.queryByRole("status")).toBeNull();
149
+ expect(screen.getByRole("list")).toBeInTheDocument();
150
+ });
151
+
152
+ it("isEmpty false manda: items vazio com emptyTitle mantém a trilha", () => {
153
+ render(<Timeline items={[]} isEmpty={false} emptyTitle="Nada por aqui" />);
154
+ expect(screen.queryByRole("status")).toBeNull();
155
+ expect(screen.getByRole("list")).toBeInTheDocument();
156
+ });
157
+
158
+ it("items com eventos e emptyTitle, sem isEmpty, mostra a trilha", () => {
159
+ render(<Timeline items={EVENTOS} emptyTitle="Nada por aqui" />);
160
+ expect(screen.queryByRole("status")).toBeNull();
161
+ expect(screen.getAllByRole("listitem")).toHaveLength(3);
162
+ });
163
+
164
+ it("o botão do vazio só aparece com rótulo e callback, e chama o callback", async () => {
165
+ const usuario = userEvent.setup();
166
+ const aoAgir = vi.fn();
167
+ const { rerender } = render(
168
+ <Timeline items={[]} isEmpty emptyActionLabel="Limpar filtro" />,
169
+ );
170
+ expect(screen.queryByRole("button")).toBeNull();
171
+ rerender(
172
+ <Timeline
173
+ items={[]}
174
+ isEmpty
175
+ emptyActionLabel="Limpar filtro"
176
+ onEmptyAction={aoAgir}
177
+ />,
178
+ );
179
+ await usuario.click(screen.getByRole("button", { name: "Limpar filtro" }));
180
+ expect(aoAgir).toHaveBeenCalledTimes(1);
181
+ });
182
+
183
+ it("o entorno não desmonta: só o miolo troca ao esvaziar e ao voltar", async () => {
184
+ const { rerender } = render(
185
+ <section>
186
+ <h3 data-testid="titulo">Histórico</h3>
187
+ <Timeline items={EVENTOS} isEmpty={false} emptyTitle="Sem eventos" />
188
+ </section>,
189
+ );
190
+ const titulo = screen.getByTestId("titulo");
191
+ expect(screen.getAllByRole("listitem")).toHaveLength(3);
192
+
193
+ rerender(
194
+ <section>
195
+ <h3 data-testid="titulo">Histórico</h3>
196
+ <Timeline items={[]} isEmpty emptyTitle="Sem eventos" />
197
+ </section>,
198
+ );
199
+ await waitFor(() => expect(screen.getByRole("status")).toBeInTheDocument());
200
+ expect(screen.getByTestId("titulo")).toBe(titulo);
201
+ expect(screen.queryByRole("list")).toBeNull();
202
+
203
+ rerender(
204
+ <section>
205
+ <h3 data-testid="titulo">Histórico</h3>
206
+ <Timeline items={EVENTOS} isEmpty={false} emptyTitle="Sem eventos" />
207
+ </section>,
208
+ );
209
+ await waitFor(() => expect(screen.getAllByRole("listitem")).toHaveLength(3));
210
+ expect(screen.getByTestId("titulo")).toBe(titulo);
211
+ expect(screen.queryByRole("status")).toBeNull();
212
+ });
213
+
214
+ it("className vai ao wrapper do vazio", () => {
215
+ render(<Timeline items={[]} isEmpty className="meu-vazio" />);
216
+ expect(screen.getByRole("status")).toHaveClass("meu-vazio");
217
+ });
218
+ });
@@ -51,7 +51,7 @@ export const normalizarEndereco = (bruto: string): string => {
51
51
  /** Cor, tamanho e família de fonte só valem como TOKEN do DS: `var(--umds-...)`. Cor em hex, rgb ou nome sai. */
52
52
  const COR_TOKEN = /^var\(--umds-(?:primitive|semantic)-[a-z0-9-]+\)$/;
53
53
  const TAMANHO_TOKEN = /^var\(--umds-primitive-font-size-[a-z0-9]+\)$/;
54
- const FAMILIA_TOKEN = /^var\(--umds-(?:primitive|semantic)-font-family-(?:base|mono)\)$/;
54
+ const FAMILIA_TOKEN = /^var\(--umds-(?:primitive|semantic)-font-family-base\)$/;
55
55
 
56
56
  /**
57
57
  * Declaração de estilo de texto que o editor aceita: `color`, `font-size` e `font-family`,