@1doc/1ds-react 2.12.2 → 2.13.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.13.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ab0ee2b: feat(breadcrumb): `truncate`, a trilha numa linha só
8
+
9
+ O `Breadcrumb` ganha `truncate`. A trilha passa a ocupar uma linha só e, quando não cabe, a página atual (o último item) encolhe
10
+ primeiro, com reticências, até 80px (separador incluído); só então os níveis anteriores encolhem. O separador nunca encolhe e o rótulo inteiro fica no
11
+ `title` de cada item. Feito para barras de altura fixa, como o `AppSubheader`, quando o último nível é um texto longo: o CrisDoc
12
+ mostra ali o número e o assunto do processo aberto, e sem isto a trilha quebrava em três linhas no celular. Sem a prop, nada muda.
13
+
14
+ ```vue
15
+ <Breadcrumb
16
+ truncate
17
+ :items="[
18
+ { label: '1Doc', href: '/' },
19
+ { label: 'Proc. Administrativo 412/2026' },
20
+ { label: assunto },
21
+ ]"
22
+ />
23
+ ```
24
+
3
25
  ## 2.12.2
4
26
 
5
27
  ### Patch Changes
@@ -70,3 +70,53 @@
70
70
  font-weight: 600;
71
71
  cursor: default;
72
72
  }
73
+
74
+ /* ── truncate: uma linha só, com reticências ──
75
+ A página atual encolhe primeiro: o `flex-shrink` alto faz o algoritmo do
76
+ flex tirar dela quase todo o espaço que falta, até a largura mínima; dali
77
+ em diante o que ainda falta se divide entre os níveis anteriores. O 100000
78
+ é uma razão de encolhimento, não uma medida, e precisa ser enorme: o flex
79
+ reparte o encolhimento na proporção (razão × largura), e com 100 os níveis
80
+ anteriores ainda levavam de 0,1 a 0,8px, o bastante para virar "1D…" e
81
+ "Emiss…" com espaço sobrando. O separador nunca encolhe.
82
+ Com rótulo final mais curto que o mínimo, o item ocupa o mínimo e a sobra
83
+ fica depois do texto, no fim da trilha, onde não aparece. */
84
+ .umds-breadcrumb--truncate {
85
+ min-width: 0;
86
+ max-width: 100%;
87
+ }
88
+
89
+ .umds-breadcrumb--truncate .umds-breadcrumb__list {
90
+ flex-wrap: nowrap;
91
+ }
92
+
93
+ .umds-breadcrumb--truncate .umds-breadcrumb__item {
94
+ min-width: 0;
95
+ white-space: nowrap;
96
+ }
97
+
98
+ .umds-breadcrumb--truncate .umds-breadcrumb__item:last-child {
99
+ flex-shrink: 100000;
100
+ min-width: var(--umds-primitive-spacing-xxl); /* 80px com o separador: "Reforma…" ainda se lê */
101
+ }
102
+
103
+ .umds-breadcrumb--truncate .umds-breadcrumb__item:only-child {
104
+ min-width: 0;
105
+ }
106
+
107
+ .umds-breadcrumb--truncate .umds-breadcrumb__sep {
108
+ flex-shrink: 0;
109
+ }
110
+
111
+ .umds-breadcrumb--truncate .umds-breadcrumb__link,
112
+ .umds-breadcrumb--truncate .umds-breadcrumb__current {
113
+ display: block;
114
+ min-width: 0;
115
+ overflow: hidden;
116
+ text-overflow: ellipsis;
117
+ }
118
+
119
+ /* Em bloco, o alvo de 24px vem da altura da linha, e o texto fica centrado. */
120
+ .umds-breadcrumb--truncate .umds-breadcrumb__link {
121
+ line-height: var(--umds-primitive-spacing-xxs); /* 24px */
122
+ }
@@ -14,6 +14,7 @@ export interface BreadcrumbItem {
14
14
  * Breadcrumb component — navegação estrutural (trilha de páginas).
15
15
  * @property {BreadcrumbItem[]} items - Lista de itens da trilha; o último é a página atual
16
16
  * @property {string} separator - Separador entre itens (padrão '/')
17
+ * @property {boolean} truncate - Trilha numa linha só, com reticências quando não cabe
17
18
  * @property {string} className - Classes CSS adicionais
18
19
  * @quandoUsar Mostrar ONDE o usuário está na hierarquia do sistema e permitir subir de nível. Use em páginas com profundidade 2 ou mais.
19
20
  * @quandoNaoUsar Para progresso dentro de um fluxo de etapas — use `Stepper`. Para navegar entre seções da mesma página, use `Tabs`.
@@ -23,6 +24,14 @@ export interface BreadcrumbProps {
23
24
  ariaLabel?: string;
24
25
  items: BreadcrumbItem[];
25
26
  separator?: string;
27
+ /**
28
+ * Mantém a trilha numa linha só. Quando não cabe, a página atual (último item)
29
+ * encolhe primeiro, com reticências, até uma largura mínima; só então os
30
+ * níveis anteriores encolhem. O rótulo inteiro fica no `title` de cada item.
31
+ * Use em barras de altura fixa (subheader) e quando o último nível é um texto
32
+ * longo, como o assunto de um processo. @default false
33
+ */
34
+ truncate?: boolean;
26
35
  className?: string;
27
36
  }
28
37
  declare function Breadcrumb(props: BreadcrumbProps): import("react/jsx-runtime").JSX.Element;
@@ -6,11 +6,20 @@ function Breadcrumb(props) {
6
6
  return props.separator || "/";
7
7
  }
8
8
  function classes() {
9
- return ["umds-breadcrumb", props.className].filter(Boolean).join(" ");
9
+ return [
10
+ "umds-breadcrumb",
11
+ props.truncate ? "umds-breadcrumb--truncate" : "",
12
+ props.className,
13
+ ]
14
+ .filter(Boolean)
15
+ .join(" ");
10
16
  }
11
17
  function itemList() {
12
18
  return props.items || [];
13
19
  }
20
+ function titleOf(item) {
21
+ return props.truncate ? item.label : undefined;
22
+ }
14
23
  function isLast(index) {
15
24
  return index === (props.items || []).length - 1;
16
25
  }
@@ -24,6 +33,6 @@ function Breadcrumb(props) {
24
33
  }
25
34
  }
26
35
  }
27
- return (_jsx("nav", { "aria-label": props.ariaLabel || "Navegação estrutural", className: classes(), children: _jsx("ol", { className: "umds-breadcrumb__list", children: itemList()?.map((item, index) => (_jsxs("li", { className: "umds-breadcrumb__item", children: [index > 0 ? (_jsx("span", { "aria-hidden": "true", className: "umds-breadcrumb__sep", children: sep() })) : null, !isLast(index) && !!item.href ? (_jsx("a", { className: "umds-breadcrumb__link", href: item.href, children: item.label })) : null, !isLast(index) && !item.href ? (_jsx("span", { role: "button", className: "umds-breadcrumb__link", tabIndex: 0, onClick: (event) => item.onClick && item.onClick(), onKeyDown: (event) => handleItemKey(event, item), children: item.label })) : null, isLast(index) ? (_jsx("span", { "aria-current": "page", className: "umds-breadcrumb__current", children: item.label })) : null] }, index))) }) }));
36
+ return (_jsx("nav", { "aria-label": props.ariaLabel || "Navegação estrutural", className: classes(), children: _jsx("ol", { className: "umds-breadcrumb__list", children: itemList()?.map((item, index) => (_jsxs("li", { className: "umds-breadcrumb__item", children: [index > 0 ? (_jsx("span", { "aria-hidden": "true", className: "umds-breadcrumb__sep", children: sep() })) : null, !isLast(index) && !!item.href ? (_jsx("a", { className: "umds-breadcrumb__link", href: item.href, title: titleOf(item), children: item.label })) : null, !isLast(index) && !item.href ? (_jsx("span", { role: "button", className: "umds-breadcrumb__link", tabIndex: 0, title: titleOf(item), onClick: (event) => item.onClick && item.onClick(), onKeyDown: (event) => handleItemKey(event, item), children: item.label })) : null, isLast(index) ? (_jsx("span", { "aria-current": "page", className: "umds-breadcrumb__current", title: titleOf(item), children: item.label })) : null] }, index))) }) }));
28
37
  }
29
38
  export default Breadcrumb;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,40 @@
1
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { describe, expect, it } from "vitest";
3
+ import Breadcrumb from "../components/Breadcrumb/Breadcrumb";
4
+ import { render } from "@testing-library/react";
5
+ import { lerArquivo } from "./lerArquivo";
6
+ const cssBreadcrumb = (await lerArquivo("../core/src/components/Breadcrumb/Breadcrumb.css")).replace(/\r\n/g, "\n");
7
+ const bloco = (texto, seletor) => {
8
+ const inicio = texto.indexOf(seletor + " {");
9
+ expect(inicio, seletor).toBeGreaterThanOrEqual(0);
10
+ return texto.slice(inicio, texto.indexOf("}", inicio));
11
+ };
12
+ const ITENS = [
13
+ { label: "1Doc", href: "#" },
14
+ { label: "Proc. Administrativo 412/2026", onClick: () => { } },
15
+ { label: "Reforma da cobertura do Ginásio Poliesportivo Municipal" },
16
+ ];
17
+ describe("Breadcrumb · truncate", () => {
18
+ it("truncate escreve a classe na raiz; sem a prop, não", () => {
19
+ const { container } = render(_jsxs(_Fragment, { children: [_jsx(Breadcrumb, { items: ITENS, truncate: true }), _jsx(Breadcrumb, { items: ITENS })] }));
20
+ expect(container.children[0]).toHaveClass("umds-breadcrumb--truncate");
21
+ expect(container.children[1]).not.toHaveClass("umds-breadcrumb--truncate");
22
+ });
23
+ it("com truncate, o rótulo inteiro vai no title do link, do item clicável e da página atual; sem, nenhum title", () => {
24
+ const { container } = render(_jsx(Breadcrumb, { items: ITENS, truncate: true }));
25
+ expect(container.querySelector("a.umds-breadcrumb__link")).toHaveAttribute("title", "1Doc");
26
+ expect(container.querySelector("[role=button].umds-breadcrumb__link")).toHaveAttribute("title", "Proc. Administrativo 412/2026");
27
+ expect(container.querySelector(".umds-breadcrumb__current")).toHaveAttribute("title", "Reforma da cobertura do Ginásio Poliesportivo Municipal");
28
+ const sem = render(_jsx(Breadcrumb, { items: ITENS }));
29
+ expect(sem.container.querySelectorAll("[title]")).toHaveLength(0);
30
+ });
31
+ it("CSS: uma linha; a página atual encolhe primeiro até 80px; o texto ganha reticências; o padrão segue quebrando linha", () => {
32
+ expect(bloco(cssBreadcrumb, ".umds-breadcrumb--truncate .umds-breadcrumb__list")).toMatch(/flex-wrap:\s*nowrap/);
33
+ const ultimo = bloco(cssBreadcrumb, ".umds-breadcrumb--truncate .umds-breadcrumb__item:last-child");
34
+ expect(ultimo).toMatch(/flex-shrink:\s*100000;/);
35
+ expect(ultimo).toMatch(/min-width:\s*var\(--umds-primitive-spacing-xxl\)/);
36
+ expect(bloco(cssBreadcrumb, ".umds-breadcrumb--truncate .umds-breadcrumb__sep")).toMatch(/flex-shrink:\s*0/);
37
+ expect(cssBreadcrumb).toMatch(/\.umds-breadcrumb--truncate \.umds-breadcrumb__current \{[^}]*text-overflow:\s*ellipsis/);
38
+ expect(bloco(cssBreadcrumb, ".umds-breadcrumb__list")).toMatch(/flex-wrap:\s*wrap/);
39
+ });
40
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1doc/1ds-react",
3
- "version": "2.12.2",
3
+ "version": "2.13.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.12.2",
49
- "@1doc/1ds-tokens": "0.7.0"
48
+ "@1doc/1ds-tokens": "0.7.0",
49
+ "@1doc/1ds-core": "2.13.0"
50
50
  },
51
51
  "publishConfig": {
52
52
  "access": "public"
@@ -0,0 +1,52 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import Breadcrumb from "../components/Breadcrumb/Breadcrumb";
3
+ import { render } from "@testing-library/react";
4
+ import { lerArquivo } from "./lerArquivo";
5
+
6
+ const cssBreadcrumb = (await lerArquivo("../core/src/components/Breadcrumb/Breadcrumb.css")).replace(/\r\n/g, "\n");
7
+ const bloco = (texto: string, seletor: string) => {
8
+ const inicio = texto.indexOf(seletor + " {");
9
+ expect(inicio, seletor).toBeGreaterThanOrEqual(0);
10
+ return texto.slice(inicio, texto.indexOf("}", inicio));
11
+ };
12
+
13
+ const ITENS = [
14
+ { label: "1Doc", href: "#" },
15
+ { label: "Proc. Administrativo 412/2026", onClick: () => {} },
16
+ { label: "Reforma da cobertura do Ginásio Poliesportivo Municipal" },
17
+ ];
18
+
19
+ describe("Breadcrumb · truncate", () => {
20
+ it("truncate escreve a classe na raiz; sem a prop, não", () => {
21
+ const { container } = render(
22
+ <>
23
+ <Breadcrumb items={ITENS} truncate />
24
+ <Breadcrumb items={ITENS} />
25
+ </>,
26
+ );
27
+ expect(container.children[0]).toHaveClass("umds-breadcrumb--truncate");
28
+ expect(container.children[1]).not.toHaveClass("umds-breadcrumb--truncate");
29
+ });
30
+
31
+ it("com truncate, o rótulo inteiro vai no title do link, do item clicável e da página atual; sem, nenhum title", () => {
32
+ const { container } = render(<Breadcrumb items={ITENS} truncate />);
33
+ expect(container.querySelector("a.umds-breadcrumb__link")).toHaveAttribute("title", "1Doc");
34
+ expect(container.querySelector("[role=button].umds-breadcrumb__link")).toHaveAttribute("title", "Proc. Administrativo 412/2026");
35
+ expect(container.querySelector(".umds-breadcrumb__current")).toHaveAttribute(
36
+ "title",
37
+ "Reforma da cobertura do Ginásio Poliesportivo Municipal",
38
+ );
39
+ const sem = render(<Breadcrumb items={ITENS} />);
40
+ expect(sem.container.querySelectorAll("[title]")).toHaveLength(0);
41
+ });
42
+
43
+ it("CSS: uma linha; a página atual encolhe primeiro até 80px; o texto ganha reticências; o padrão segue quebrando linha", () => {
44
+ expect(bloco(cssBreadcrumb, ".umds-breadcrumb--truncate .umds-breadcrumb__list")).toMatch(/flex-wrap:\s*nowrap/);
45
+ const ultimo = bloco(cssBreadcrumb, ".umds-breadcrumb--truncate .umds-breadcrumb__item:last-child");
46
+ expect(ultimo).toMatch(/flex-shrink:\s*100000;/);
47
+ expect(ultimo).toMatch(/min-width:\s*var\(--umds-primitive-spacing-xxl\)/);
48
+ expect(bloco(cssBreadcrumb, ".umds-breadcrumb--truncate .umds-breadcrumb__sep")).toMatch(/flex-shrink:\s*0/);
49
+ expect(cssBreadcrumb).toMatch(/\.umds-breadcrumb--truncate \.umds-breadcrumb__current \{[^}]*text-overflow:\s*ellipsis/);
50
+ expect(bloco(cssBreadcrumb, ".umds-breadcrumb__list")).toMatch(/flex-wrap:\s*wrap/);
51
+ });
52
+ });