@1doc/1ds-react 0.0.2 → 2.7.1

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 (101) hide show
  1. package/CHANGELOG.md +659 -0
  2. package/README.md +137 -6
  3. package/package.json +43 -28
  4. package/src/index.ts +161 -16
  5. package/src/teste/AttachmentCard.test.tsx +161 -0
  6. package/src/teste/Chart.test.tsx +233 -0
  7. package/src/teste/Combobox.test.tsx +251 -0
  8. package/src/teste/DataTable.test.tsx +70 -0
  9. package/src/teste/DatePicker.test.tsx +86 -0
  10. package/src/teste/Drawer.test.tsx +265 -0
  11. package/src/teste/Dropdown.test.tsx +217 -0
  12. package/src/teste/Input.test.tsx +64 -0
  13. package/src/teste/Kanban.test.tsx +333 -0
  14. package/src/teste/MultiSelect.test.tsx +73 -0
  15. package/src/teste/Podium.test.tsx +107 -0
  16. package/src/teste/Popover.test.tsx +221 -0
  17. package/src/teste/SegmentedControl.test.tsx +183 -0
  18. package/src/teste/Timeline.test.tsx +100 -0
  19. package/src/teste/Tree.test.tsx +74 -0
  20. package/src/teste/setup.ts +7 -0
  21. package/src/utils/bandeiras.ts +22 -0
  22. package/src/utils/chart.ts +682 -0
  23. package/src/utils/density.ts +27 -0
  24. package/src/utils/overlay-stack.ts +26 -0
  25. package/src/utils/theme.ts +63 -47
  26. package/src/utils/toast.ts +89 -0
  27. package/vitest.config.ts +28 -0
  28. package/dist/components/Alert/Alert.css +0 -188
  29. package/dist/components/Alert/Alert.d.ts +0 -65
  30. package/dist/components/Alert/Alert.js +0 -56
  31. package/dist/components/Avatar/Avatar.css +0 -88
  32. package/dist/components/Avatar/Avatar.d.ts +0 -34
  33. package/dist/components/Avatar/Avatar.js +0 -16
  34. package/dist/components/Avatar/AvatarGroup.d.ts +0 -12
  35. package/dist/components/Avatar/AvatarGroup.js +0 -8
  36. package/dist/components/Badge/Badge.css +0 -155
  37. package/dist/components/Badge/Badge.d.ts +0 -46
  38. package/dist/components/Badge/Badge.js +0 -17
  39. package/dist/components/Button/Button.css +0 -199
  40. package/dist/components/Button/Button.d.ts +0 -23
  41. package/dist/components/Button/Button.js +0 -62
  42. package/dist/components/Checkbox/Checkbox.css +0 -122
  43. package/dist/components/Checkbox/Checkbox.d.ts +0 -21
  44. package/dist/components/Checkbox/Checkbox.js +0 -49
  45. package/dist/components/Divider/Divider.css +0 -81
  46. package/dist/components/Divider/Divider.d.ts +0 -18
  47. package/dist/components/Divider/Divider.js +0 -26
  48. package/dist/components/Grid/Grid.css +0 -154
  49. package/dist/components/Grid/Grid.d.ts +0 -21
  50. package/dist/components/Grid/Grid.js +0 -53
  51. package/dist/components/Icon/Icon.css +0 -55
  52. package/dist/components/Icon/Icon.d.ts +0 -13
  53. package/dist/components/Icon/Icon.js +0 -41
  54. package/dist/components/Input/Input.css +0 -190
  55. package/dist/components/Input/Input.d.ts +0 -32
  56. package/dist/components/Input/Input.js +0 -58
  57. package/dist/components/Link/Link.css +0 -76
  58. package/dist/components/Link/Link.d.ts +0 -15
  59. package/dist/components/Link/Link.js +0 -19
  60. package/dist/components/Logo/Logo.css +0 -32
  61. package/dist/components/Logo/Logo.d.ts +0 -10
  62. package/dist/components/Logo/Logo.js +0 -16
  63. package/dist/components/Modal/Modal.css +0 -155
  64. package/dist/components/Modal/Modal.d.ts +0 -36
  65. package/dist/components/Modal/Modal.js +0 -55
  66. package/dist/components/Radio/Radio.css +0 -114
  67. package/dist/components/Radio/Radio.d.ts +0 -19
  68. package/dist/components/Radio/Radio.js +0 -39
  69. package/dist/components/Select/Select.css +0 -291
  70. package/dist/components/Select/Select.d.ts +0 -38
  71. package/dist/components/Select/Select.js +0 -246
  72. package/dist/components/Spinner/Spinner.css +0 -60
  73. package/dist/components/Spinner/Spinner.d.ts +0 -16
  74. package/dist/components/Spinner/Spinner.js +0 -16
  75. package/dist/components/Switch/Switch.css +0 -127
  76. package/dist/components/Switch/Switch.d.ts +0 -18
  77. package/dist/components/Switch/Switch.js +0 -57
  78. package/dist/components/Tabs/Tab.d.ts +0 -19
  79. package/dist/components/Tabs/Tab.js +0 -76
  80. package/dist/components/Tabs/TabContent.d.ts +0 -15
  81. package/dist/components/Tabs/TabContent.js +0 -11
  82. package/dist/components/Tabs/Tabs.css +0 -124
  83. package/dist/components/Tabs/Tabs.d.ts +0 -20
  84. package/dist/components/Tabs/Tabs.js +0 -14
  85. package/dist/components/Tabs/TabsContent.d.ts +0 -16
  86. package/dist/components/Tabs/TabsContent.js +0 -12
  87. package/dist/components/Tabs/tabs.context.d.ts +0 -2
  88. package/dist/components/Tabs/tabs.context.js +0 -2
  89. package/dist/components/Tag/Tag.css +0 -140
  90. package/dist/components/Tag/Tag.d.ts +0 -63
  91. package/dist/components/Tag/Tag.js +0 -48
  92. package/dist/components/Text/Text.css +0 -1213
  93. package/dist/components/Text/Text.d.ts +0 -70
  94. package/dist/components/Text/Text.js +0 -89
  95. package/dist/components/Textarea/Textarea.css +0 -133
  96. package/dist/components/Textarea/Textarea.d.ts +0 -28
  97. package/dist/components/Textarea/Textarea.js +0 -72
  98. package/dist/index.d.ts +0 -25
  99. package/dist/index.js +0 -27
  100. package/dist/utils/theme.d.ts +0 -30
  101. package/dist/utils/theme.js +0 -106
@@ -0,0 +1,682 @@
1
+ /**
2
+ * Geometria dos gráficos do 1DS (BarChart, LineChart, DonutChart).
3
+ *
4
+ * Módulo puro, sem DOM: recebe dados e medidas, devolve coordenadas prontas
5
+ * para o SVG. Mora fora dos `.lite.tsx` por dois motivos:
6
+ *
7
+ * - o Mitosis não aceita função em escopo de módulo dentro de um `.lite.tsx`
8
+ * (tenta ler a declaração como JSON5 e explode);
9
+ * - os três gráficos compartilham a escala, o formatador e a medição de
10
+ * texto — cópia em três arquivos seria três lugares para corrigir.
11
+ *
12
+ * Nada aqui escolhe cor. A cor de cada série é uma classe CSS
13
+ * (`umds-chart__serie--N`) que aponta para um token semântico — é assim que o
14
+ * gráfico acompanha o tema escuro sem nenhuma linha de JavaScript.
15
+ */
16
+
17
+ /** Um ponto de uma série única: rótulo da categoria e valor. */
18
+ export interface ChartPonto {
19
+ label: string;
20
+ value: number | null;
21
+ }
22
+
23
+ /** Uma série nomeada, com um valor por categoria. */
24
+ export interface ChartSerieBruta {
25
+ name: string;
26
+ values: Array<number | null>;
27
+ }
28
+
29
+ /**
30
+ * Quantas séries (ou fatias) cabem com cor própria.
31
+ *
32
+ * O DS não tem paleta categórica: as cinco cores saem dos tokens semânticos
33
+ * que já existem e que passaram no validador de daltonismo nos temas 1doc e
34
+ * 1doc-dark. Uma sexta série não ganha cor gerada — gráfico com mais séries
35
+ * pede small multiples ou agrupamento em "Outros".
36
+ */
37
+ export const CHART_MAX_SERIES = 5;
38
+
39
+ /** Altura de linha do texto dos eixos (12px da escala oficial). */
40
+ const FONTE_EIXO = 12;
41
+ /** Largura média de um caractere de Open Sans a 12px, com folga. */
42
+ const LARGURA_CARACTERE = 7;
43
+ export interface ChartFormato {
44
+ valueFormatter?: (value: number) => string;
45
+ unit?: string;
46
+ }
47
+
48
+ /** Formata um valor para leitura: formatador do consumidor ou pt-BR + unidade. */
49
+ export function formatarValor(valor: number | null | undefined, formato: ChartFormato): string {
50
+ if (valor === null || valor === undefined || Number.isNaN(valor)) return "—";
51
+ if (typeof formato.valueFormatter === "function") return formato.valueFormatter(valor);
52
+ const numero = valor.toLocaleString("pt-BR", {
53
+ maximumFractionDigits: 2
54
+ });
55
+ const unidade = formato.unit || "";
56
+ if (!unidade) return numero;
57
+ // "%" encosta no número (12%); qualquer outra unidade leva espaço (12 dias).
58
+ return unidade === "%" ? `${numero}%` : `${numero} ${unidade}`;
59
+ }
60
+
61
+ /**
62
+ * O mesmo valor, para leitor de tela: "—" é lido como "travessão" (ou nada),
63
+ * então ausência de dado vira "sem dado".
64
+ */
65
+ export function valorAcessivel(valor: number | null | undefined, formato: ChartFormato): string {
66
+ if (valor === null || valor === undefined || Number.isNaN(valor)) return "sem dado";
67
+ return formatarValor(valor, formato);
68
+ }
69
+
70
+ /** Estimativa da largura de um texto de eixo, sem medir no DOM. */
71
+ export function larguraDoTexto(texto: string): number {
72
+ return (texto || "").length * LARGURA_CARACTERE;
73
+ }
74
+
75
+ /** Arredonda o erro de ponto flutuante de somas como 0,1 + 0,2. */
76
+ function limpar(numero: number): number {
77
+ return Math.round(numero * 1e9) / 1e9;
78
+ }
79
+
80
+ /** Passo "redondo" (1, 2, 2,5, 5 × 10^n) mais próximo acima do bruto. */
81
+ function passoRedondo(bruto: number): number {
82
+ if (bruto <= 0 || !Number.isFinite(bruto)) return 1;
83
+ const expoente = Math.floor(Math.log10(bruto));
84
+ const base = Math.pow(10, expoente);
85
+ const fracao = bruto / base;
86
+ const redondo = fracao <= 1 ? 1 : fracao <= 2 ? 2 : fracao <= 2.5 ? 2.5 : fracao <= 5 ? 5 : 10;
87
+ return redondo * base;
88
+ }
89
+ export interface ChartEscala {
90
+ min: number;
91
+ max: number;
92
+ ticks: number[];
93
+ }
94
+
95
+ /**
96
+ * Escala do eixo Y com números redondos, sempre incluindo o zero: barra que
97
+ * não nasce do zero mente sobre a proporção.
98
+ */
99
+ export function escalaRedonda(valores: number[], divisoes = 4): ChartEscala {
100
+ let min = 0;
101
+ let max = 0;
102
+ for (const v of valores) {
103
+ if (v < min) min = v;
104
+ if (v > max) max = v;
105
+ }
106
+ if (min === max) max = min + 1;
107
+ const passo = passoRedondo((max - min) / divisoes);
108
+ const inicio = Math.floor(limpar(min / passo)) * passo;
109
+ const fim = Math.ceil(limpar(max / passo)) * passo;
110
+ const ticks: number[] = [];
111
+ for (let t = inicio; t <= fim + passo / 2; t += passo) ticks.push(limpar(t));
112
+ return {
113
+ min: limpar(inicio),
114
+ max: limpar(fim),
115
+ ticks
116
+ };
117
+ }
118
+ export interface ChartDados {
119
+ categorias: string[];
120
+ series: ChartSerieBruta[];
121
+ }
122
+
123
+ /**
124
+ * Junta as duas formas de entrada numa só: `data` (série única de
125
+ * `{ label, value }`) ou `series` + `labels`. Corta em CHART_MAX_SERIES.
126
+ */
127
+ export function normalizarDados(data: ChartPonto[] | undefined, series: ChartSerieBruta[] | undefined, labels: string[] | undefined, nomePadrao: string): ChartDados {
128
+ if (series && series.length > 0) {
129
+ const cortadas = series.slice(0, CHART_MAX_SERIES);
130
+ let n = 0;
131
+ for (const s of cortadas) n = Math.max(n, (s.values || []).length);
132
+ const categorias: string[] = [];
133
+ for (let i = 0; i < n; i++) {
134
+ categorias.push(labels && labels[i] !== undefined ? String(labels[i]) : String(i + 1));
135
+ }
136
+ return {
137
+ categorias,
138
+ series: cortadas.map(s => ({
139
+ name: s.name,
140
+ values: categorias.map((_, i) => {
141
+ const v = (s.values || [])[i];
142
+ return typeof v === "number" && Number.isFinite(v) ? v : null;
143
+ })
144
+ }))
145
+ };
146
+ }
147
+ const pontos = data || [];
148
+ return {
149
+ categorias: pontos.map(p => String(p.label)),
150
+ series: pontos.length ? [{
151
+ name: nomePadrao,
152
+ values: pontos.map(p => typeof p.value === "number" && Number.isFinite(p.value) ? p.value : null)
153
+ }] : []
154
+ };
155
+ }
156
+
157
+ /** Existe ao menos um número para desenhar? */
158
+ export function temValores(dados: ChartDados): boolean {
159
+ return dados.series.some(s => s.values.some(v => v !== null));
160
+ }
161
+
162
+ // ─── Plano cartesiano (barras e linhas) ───────────────────────────────
163
+
164
+ export interface ChartTick {
165
+ y: number;
166
+ texto: string;
167
+ }
168
+ export interface ChartRotuloX {
169
+ x: number;
170
+ texto: string;
171
+ }
172
+ export interface ChartLinhaDoTooltip {
173
+ serie: number;
174
+ nome: string;
175
+ texto: string;
176
+ }
177
+
178
+ /** Área de interação de uma categoria: posição em % e o que o tooltip lê. */
179
+ export interface ChartAlvo {
180
+ indice: number;
181
+ categoria: string;
182
+ esquerda: string;
183
+ largura: string;
184
+ lado: "direita" | "esquerda";
185
+ descricao: string;
186
+ linhas: ChartLinhaDoTooltip[];
187
+ }
188
+ export interface ChartPlano {
189
+ largura: number;
190
+ altura: number;
191
+ topo: number;
192
+ base: number;
193
+ esquerda: number;
194
+ direita: number;
195
+ banda: number;
196
+ y0: number;
197
+ ticks: ChartTick[];
198
+ rotulosX: ChartRotuloX[];
199
+ alvos: ChartAlvo[];
200
+ escalaY: (valor: number) => number;
201
+ centroX: (indice: number) => number;
202
+ }
203
+
204
+ /**
205
+ * Monta o plano: margens medidas pelo texto dos ticks, escala Y redonda,
206
+ * rótulos do X rareados quando não cabem, e uma área de interação por
207
+ * categoria.
208
+ */
209
+ export function montarPlano(dados: ChartDados, largura: number, altura: number, formato: ChartFormato): ChartPlano {
210
+ const valores: number[] = [];
211
+ for (const s of dados.series) for (const v of s.values) if (v !== null) valores.push(v);
212
+ const escala = escalaRedonda(valores);
213
+ const textosTick = escala.ticks.map(t => formatarValor(t, formato));
214
+ let maiorTick = 0;
215
+ for (const t of textosTick) maiorTick = Math.max(maiorTick, larguraDoTexto(t));
216
+ const topo = FONTE_EIXO / 2 + 2;
217
+ const esquerda = Math.ceil(maiorTick) + 8;
218
+ const n = Math.max(1, dados.categorias.length);
219
+ // O último rótulo do X é centrado na última banda e pode passar da borda
220
+ // direita; a margem cresce só o que ele passar.
221
+ const bandaPrevia = Math.max(1, largura - esquerda - 8) / n;
222
+ const ultimo = larguraDoTexto(dados.categorias[dados.categorias.length - 1] || "");
223
+ const direita = Math.max(8, Math.ceil(ultimo / 2 - bandaPrevia / 2) + 2);
224
+ const base = altura - (FONTE_EIXO + 12);
225
+ const larguraUtil = Math.max(1, largura - esquerda - direita);
226
+ const banda = larguraUtil / n;
227
+ const escalaY = (valor: number) => topo + (escala.max - valor) / (escala.max - escala.min) * (base - topo);
228
+ const centroX = (indice: number) => esquerda + banda * indice + banda / 2;
229
+ const ticks = escala.ticks.map((t, i) => ({
230
+ y: escalaY(t),
231
+ texto: textosTick[i]
232
+ }));
233
+ let maiorRotulo = 0;
234
+ for (const c of dados.categorias) maiorRotulo = Math.max(maiorRotulo, larguraDoTexto(c));
235
+ const pulo = Math.max(1, Math.ceil((maiorRotulo + 8) / banda));
236
+ const rotulosX: ChartRotuloX[] = [];
237
+ dados.categorias.forEach((c, i) => {
238
+ if (i % pulo === 0) rotulosX.push({
239
+ x: centroX(i),
240
+ texto: c
241
+ });
242
+ });
243
+ const alvos: ChartAlvo[] = dados.categorias.map((categoria, i) => {
244
+ const linhas = dados.series.map((s, j) => ({
245
+ serie: j + 1,
246
+ nome: s.name,
247
+ texto: formatarValor(s.values[i], formato)
248
+ }));
249
+ const partes = dados.series.map(s => {
250
+ const texto = valorAcessivel(s.values[i], formato);
251
+ return dados.series.length > 1 ? `${s.name}: ${texto}` : texto;
252
+ });
253
+ return {
254
+ indice: i,
255
+ categoria,
256
+ esquerda: `${(esquerda + banda * i) / largura * 100}%`,
257
+ largura: `${banda / largura * 100}%`,
258
+ lado: i < n / 2 ? "direita" : "esquerda",
259
+ descricao: `${categoria}: ${partes.join("; ")}`,
260
+ linhas
261
+ };
262
+ });
263
+ return {
264
+ largura,
265
+ altura,
266
+ topo,
267
+ base,
268
+ esquerda,
269
+ direita,
270
+ banda,
271
+ y0: escalaY(0),
272
+ ticks,
273
+ rotulosX,
274
+ alvos,
275
+ escalaY,
276
+ centroX
277
+ };
278
+ }
279
+
280
+ /**
281
+ * Raio da ponta da barra, em px. Espelha `--umds-primitive-border-radius-sm`
282
+ * (8px), o raio universal do DS. Precisa ser número porque o arredondamento
283
+ * é geometria do caminho SVG, calculada aqui — não é um token novo.
284
+ */
285
+ export const RAIO_DA_BARRA = 8;
286
+
287
+ /**
288
+ * Caminho de uma barra: quadrada na linha de base, arredondada na ponta do
289
+ * dado com o raio canônico. Valor negativo arredonda embaixo. O raio nunca
290
+ * passa de metade da largura (barra fina não vira pílula) nem de metade da
291
+ * altura (barra curta não estoura a linha de base).
292
+ */
293
+ export function caminhoDaBarra(x: number, largura: number, y0: number, y: number): string {
294
+ const altura = Math.abs(y0 - y);
295
+ if (altura < 0.5 || largura <= 0) return "";
296
+ const r = Math.min(RAIO_DA_BARRA, largura / 2, altura / 2);
297
+ const x1 = x + largura;
298
+ if (y <= y0) {
299
+ return `M${x},${y0}V${y + r}A${r},${r} 0 0 1 ${x + r},${y}H${x1 - r}A${r},${r} 0 0 1 ${x1},${y + r}V${y0}Z`;
300
+ }
301
+ return `M${x},${y0}V${y - r}A${r},${r} 0 0 0 ${x + r},${y}H${x1 - r}A${r},${r} 0 0 0 ${x1},${y - r}V${y0}Z`;
302
+ }
303
+ export interface ChartBarra {
304
+ chave: string;
305
+ serie: number;
306
+ indice: number;
307
+ d: string;
308
+ }
309
+
310
+ /** Barras verticais, agrupadas lado a lado quando há mais de uma série. */
311
+ export function montarBarras(dados: ChartDados, plano: ChartPlano): ChartBarra[] {
312
+ const m = Math.max(1, dados.series.length);
313
+ const vao = 2;
314
+ const espessura = Math.max(2, Math.min(24, (plano.banda * 0.72 - vao * (m - 1)) / m));
315
+ const grupo = espessura * m + vao * (m - 1);
316
+ const barras: ChartBarra[] = [];
317
+ dados.categorias.forEach((_, i) => {
318
+ const inicio = plano.esquerda + plano.banda * i + (plano.banda - grupo) / 2;
319
+ dados.series.forEach((s, j) => {
320
+ const v = s.values[i];
321
+ if (v === null) return;
322
+ const d = caminhoDaBarra(inicio + j * (espessura + vao), espessura, plano.y0, plano.escalaY(v));
323
+ if (d) barras.push({
324
+ chave: `${j}-${i}`,
325
+ serie: j + 1,
326
+ indice: i,
327
+ d
328
+ });
329
+ });
330
+ });
331
+ return barras;
332
+ }
333
+ export interface ChartPontoDaLinha {
334
+ chave: string;
335
+ serie: number;
336
+ indice: number;
337
+ x: number;
338
+ y: number;
339
+ }
340
+ export interface ChartTraco {
341
+ chave: string;
342
+ serie: number;
343
+ d: string;
344
+ pontos: ChartPontoDaLinha[];
345
+ }
346
+
347
+ /** Uma linha por série; valor ausente (`null`) interrompe o traço. */
348
+ export function montarLinhas(dados: ChartDados, plano: ChartPlano): ChartTraco[] {
349
+ return dados.series.map((s, j) => {
350
+ let d = "";
351
+ let caneta = false;
352
+ const pontos: ChartPontoDaLinha[] = [];
353
+ s.values.forEach((v, i) => {
354
+ if (v === null) {
355
+ caneta = false;
356
+ return;
357
+ }
358
+ const x = plano.centroX(i);
359
+ const y = plano.escalaY(v);
360
+ d += `${caneta ? "L" : "M"}${x},${y}`;
361
+ caneta = true;
362
+ pontos.push({
363
+ chave: `${j}-${i}`,
364
+ serie: j + 1,
365
+ indice: i,
366
+ x,
367
+ y
368
+ });
369
+ });
370
+ return {
371
+ chave: String(j),
372
+ serie: j + 1,
373
+ d,
374
+ pontos
375
+ };
376
+ });
377
+ }
378
+
379
+ /** Frase curta para o `aria-label` do gráfico: o que é e a faixa de valores. */
380
+ export function resumoCartesiano(tipo: string, dados: ChartDados, formato: ChartFormato): string {
381
+ let min = Infinity;
382
+ let max = -Infinity;
383
+ for (const s of dados.series) for (const v of s.values) if (v !== null) {
384
+ if (v < min) min = v;
385
+ if (v > max) max = v;
386
+ }
387
+ const n = dados.categorias.length;
388
+ const m = dados.series.length;
389
+ const series = m > 1 ? `, ${m} séries (${dados.series.map(s => s.name).join(", ")})` : "";
390
+ const faixa = min === Infinity ? "" : ` Valores de ${formatarValor(min, formato)} a ${formatarValor(max, formato)}.`;
391
+ return `${tipo} com ${n} ${n === 1 ? "categoria" : "categorias"}${series}.${faixa}`;
392
+ }
393
+
394
+ // ─── Rosca ────────────────────────────────────────────────────────────
395
+
396
+ export interface ChartFatia {
397
+ chave: string;
398
+ /** Posição da cor (1 a CHART_MAX_SERIES); 0 é a fatia "Outros", em neutro. */
399
+ serie: number;
400
+ indice: number;
401
+ nome: string;
402
+ valor: number;
403
+ texto: string;
404
+ percentual: string;
405
+ d: string;
406
+ }
407
+ export interface ChartRosca {
408
+ total: number;
409
+ fatias: ChartFatia[];
410
+ }
411
+ function pontoNoCirculo(c: number, r: number, angulo: number): string {
412
+ return `${c + r * Math.cos(angulo)},${c + r * Math.sin(angulo)}`;
413
+ }
414
+
415
+ /** Setor de anel entre dois ângulos (radianos, zero às 12h, sentido horário). */
416
+ export function caminhoDoSetor(c: number, raio: number, interno: number, a0: number, a1: number): string {
417
+ // Um setor de 360° degenera (início = fim); desenha como duas metades.
418
+ if (a1 - a0 >= Math.PI * 2 - 1e-6) {
419
+ const meio = a0 + Math.PI;
420
+ return caminhoDoSetor(c, raio, interno, a0, meio) + caminhoDoSetor(c, raio, interno, meio, a1);
421
+ }
422
+ const g0 = a0 - Math.PI / 2;
423
+ const g1 = a1 - Math.PI / 2;
424
+ const grande = a1 - a0 > Math.PI ? 1 : 0;
425
+ return `M${pontoNoCirculo(c, raio, g0)}` + `A${raio},${raio} 0 ${grande} 1 ${pontoNoCirculo(c, raio, g1)}` + `L${pontoNoCirculo(c, interno, g1)}` + `A${interno},${interno} 0 ${grande} 0 ${pontoNoCirculo(c, interno, g0)}Z`;
426
+ }
427
+
428
+ /**
429
+ * Fatias da rosca. Mais que CHART_MAX_SERIES partes: as primeiras ficam e o
430
+ * resto vira uma fatia só, `rotuloOutros`. Valor negativo ou ausente conta
431
+ * como zero — parte de um todo não pode ser negativa.
432
+ */
433
+ export function montarRosca(data: ChartPonto[] | undefined, tamanho: number, formato: ChartFormato, rotuloOutros: string): ChartRosca {
434
+ const limpos = (data || []).map(p => ({
435
+ nome: String(p.label),
436
+ valor: typeof p.value === "number" && p.value > 0 ? p.value : 0
437
+ }));
438
+ let partes = limpos;
439
+ let agrupou = false;
440
+ if (limpos.length > CHART_MAX_SERIES) {
441
+ const ficam = limpos.slice(0, CHART_MAX_SERIES - 1);
442
+ const resto = limpos.slice(CHART_MAX_SERIES - 1).reduce((soma, p) => soma + p.valor, 0);
443
+ partes = [...ficam, {
444
+ nome: rotuloOutros,
445
+ valor: resto
446
+ }];
447
+ agrupou = true;
448
+ }
449
+ const total = partes.reduce((soma, p) => soma + p.valor, 0);
450
+ const c = tamanho / 2;
451
+ const raio = c - 1;
452
+ const interno = raio * 0.64;
453
+ let angulo = 0;
454
+ const fatias = partes.map((p, i) => {
455
+ const fracao = total > 0 ? p.valor / total : 0;
456
+ const a0 = angulo;
457
+ const a1 = angulo + fracao * Math.PI * 2;
458
+ angulo = a1;
459
+ return {
460
+ chave: String(i),
461
+ serie: agrupou && i === partes.length - 1 ? 0 : i + 1,
462
+ indice: i,
463
+ nome: p.nome,
464
+ valor: p.valor,
465
+ texto: formatarValor(p.valor, formato),
466
+ percentual: `${(fracao * 100).toLocaleString("pt-BR", {
467
+ maximumFractionDigits: 1
468
+ })}%`,
469
+ d: fracao > 0 ? caminhoDoSetor(c, raio, interno, a0, a1) : ""
470
+ };
471
+ });
472
+ return {
473
+ total,
474
+ fatias
475
+ };
476
+ }
477
+
478
+ // ─── Modelos prontos para o template ─────────────────────────────────
479
+ //
480
+ // O template de um `.lite.tsx` só lê campos destes objetos. Nenhum getter do
481
+ // componente lê outro getter (na geração Vue isso perde o `.value`), então
482
+ // tudo o que o desenho precisa é calculado aqui, de uma vez.
483
+
484
+ export interface ChartEntradaCartesiana extends ChartFormato {
485
+ data?: ChartPonto[];
486
+ series?: ChartSerieBruta[];
487
+ labels?: string[];
488
+ seriesName?: string;
489
+ title?: string;
490
+ ariaLabel?: string;
491
+ showLegend?: any;
492
+ showGrid?: any;
493
+ }
494
+ export interface ChartGradeLinha {
495
+ chave: string;
496
+ x1: number;
497
+ x2: number;
498
+ y: number;
499
+ xTexto: number;
500
+ texto: string;
501
+ }
502
+ export interface ChartRotuloXPosicionado {
503
+ chave: string;
504
+ x: number;
505
+ y: number;
506
+ texto: string;
507
+ }
508
+ export interface ChartAlvoPosicionado extends ChartAlvo {
509
+ cx: number;
510
+ }
511
+ export interface ChartItemDeLegenda {
512
+ chave: string;
513
+ serie: number;
514
+ nome: string;
515
+ }
516
+ export interface ChartLinhaDaTabela {
517
+ chave: string;
518
+ categoria: string;
519
+ valores: Array<{
520
+ chave: string;
521
+ texto: string;
522
+ }>;
523
+ }
524
+ export interface ChartModeloCartesiano {
525
+ vazio: boolean;
526
+ viewBox: string;
527
+ largura: number;
528
+ altura: number;
529
+ topo: number;
530
+ base: number;
531
+ esquerda: number;
532
+ fimX: number;
533
+ y0: number;
534
+ mostraGrade: boolean;
535
+ mostraLegenda: boolean;
536
+ grade: ChartGradeLinha[];
537
+ rotulosX: ChartRotuloXPosicionado[];
538
+ alvos: ChartAlvoPosicionado[];
539
+ barras: ChartBarra[];
540
+ tracos: ChartTraco[];
541
+ pontos: ChartPontoDaLinha[];
542
+ legenda: ChartItemDeLegenda[];
543
+ resumo: string;
544
+ rotuloDaLista: string;
545
+ legendaDaTabela: string;
546
+ cabecalhos: ChartItemDeLegenda[];
547
+ tabela: ChartLinhaDaTabela[];
548
+ }
549
+
550
+ /** Prop booleana que liga por padrão: só `false` desliga (Vue entrega `undefined` sem `boolean`). */
551
+ function ligadoPorPadrao(valor: any): boolean {
552
+ return valor !== false && valor !== "false";
553
+ }
554
+ export function modeloCartesiano(tipo: "barras" | "linhas", entrada: ChartEntradaCartesiana, largura: number, altura: number): ChartModeloCartesiano {
555
+ const formato: ChartFormato = {
556
+ valueFormatter: entrada.valueFormatter,
557
+ unit: entrada.unit
558
+ };
559
+ const dados = normalizarDados(entrada.data, entrada.series, entrada.labels, entrada.seriesName || entrada.title || "Valor");
560
+ const largo = Math.max(120, Math.round(largura || 0));
561
+ const alto = Math.max(80, Math.round(altura || 0));
562
+ const legenda = dados.series.map((s, j) => ({
563
+ chave: String(j),
564
+ serie: j + 1,
565
+ nome: s.name
566
+ }));
567
+ const nomeDoTipo = tipo === "barras" ? "Gráfico de barras" : "Gráfico de linhas";
568
+ const base: ChartModeloCartesiano = {
569
+ vazio: true,
570
+ viewBox: `0 0 ${largo} ${alto}`,
571
+ largura: largo,
572
+ altura: alto,
573
+ topo: 0,
574
+ base: alto,
575
+ esquerda: 0,
576
+ fimX: largo,
577
+ y0: alto,
578
+ mostraGrade: ligadoPorPadrao(entrada.showGrid),
579
+ mostraLegenda: legenda.length > 1 && ligadoPorPadrao(entrada.showLegend),
580
+ grade: [],
581
+ rotulosX: [],
582
+ alvos: [],
583
+ barras: [],
584
+ tracos: [],
585
+ pontos: [],
586
+ legenda,
587
+ resumo: entrada.ariaLabel || `${nomeDoTipo} sem dados.`,
588
+ rotuloDaLista: entrada.title ? `Valores por categoria — ${entrada.title}` : "Valores por categoria",
589
+ legendaDaTabela: entrada.title || nomeDoTipo,
590
+ cabecalhos: legenda,
591
+ tabela: []
592
+ };
593
+ if (!temValores(dados)) return base;
594
+ const plano = montarPlano(dados, largo, alto, formato);
595
+ const fimX = largo - plano.direita;
596
+ const tracos = tipo === "linhas" ? montarLinhas(dados, plano) : [];
597
+ const pontos: ChartPontoDaLinha[] = [];
598
+ for (const t of tracos) for (const p of t.pontos) pontos.push(p);
599
+ return {
600
+ ...base,
601
+ vazio: false,
602
+ topo: plano.topo,
603
+ base: plano.base,
604
+ esquerda: plano.esquerda,
605
+ fimX,
606
+ y0: plano.y0,
607
+ grade: plano.ticks.map((t, i) => ({
608
+ chave: String(i),
609
+ x1: plano.esquerda,
610
+ x2: fimX,
611
+ y: t.y,
612
+ xTexto: plano.esquerda - 8,
613
+ texto: t.texto
614
+ })),
615
+ rotulosX: plano.rotulosX.map((r, i) => ({
616
+ chave: String(i),
617
+ x: r.x,
618
+ y: plano.base + 8,
619
+ texto: r.texto
620
+ })),
621
+ alvos: plano.alvos.map(a => ({
622
+ ...a,
623
+ cx: plano.centroX(a.indice)
624
+ })),
625
+ barras: tipo === "barras" ? montarBarras(dados, plano) : [],
626
+ tracos,
627
+ pontos,
628
+ // Sem o título: a <figcaption> já dá nome à figura, e repetir aqui faria o
629
+ // leitor de tela anunciar o título duas vezes.
630
+ resumo: entrada.ariaLabel || resumoCartesiano(nomeDoTipo, dados, formato),
631
+ tabela: dados.categorias.map((categoria, i) => ({
632
+ chave: String(i),
633
+ categoria,
634
+ valores: dados.series.map((s, j) => ({
635
+ chave: String(j),
636
+ texto: valorAcessivel(s.values[i], formato)
637
+ }))
638
+ }))
639
+ };
640
+ }
641
+ export interface ChartEntradaRosca extends ChartFormato {
642
+ data?: ChartPonto[];
643
+ title?: string;
644
+ ariaLabel?: string;
645
+ centerLabel?: string;
646
+ centerValue?: string;
647
+ otherLabel?: string;
648
+ showLegend?: any;
649
+ }
650
+ export interface ChartModeloRosca {
651
+ vazio: boolean;
652
+ tamanho: number;
653
+ viewBox: string;
654
+ fatias: ChartFatia[];
655
+ centroValor: string;
656
+ centroRotulo: string;
657
+ mostraLegenda: boolean;
658
+ resumo: string;
659
+ }
660
+ export function modeloRosca(entrada: ChartEntradaRosca, tamanho: number): ChartModeloRosca {
661
+ const formato: ChartFormato = {
662
+ valueFormatter: entrada.valueFormatter,
663
+ unit: entrada.unit
664
+ };
665
+ const lado = Math.max(96, Math.round(tamanho || 0));
666
+ const rosca = montarRosca(entrada.data, lado, formato, entrada.otherLabel || "Outros");
667
+ const centroValor = entrada.centerValue || formatarValor(rosca.total, formato);
668
+ const centroRotulo = entrada.centerLabel === undefined ? "Total" : entrada.centerLabel;
669
+ return {
670
+ vazio: rosca.total <= 0,
671
+ tamanho: lado,
672
+ viewBox: `0 0 ${lado} ${lado}`,
673
+ fatias: rosca.fatias,
674
+ centroValor,
675
+ centroRotulo,
676
+ mostraLegenda: ligadoPorPadrao(entrada.showLegend),
677
+ // Curto de propósito: as partes, com valor e percentual, estão na legenda
678
+ // (visível ou oculta), que é a leitura principal. Repetir aqui faria o
679
+ // leitor de tela ler tudo duas vezes.
680
+ resumo: entrada.ariaLabel || (rosca.total > 0 ? `Gráfico de rosca com ${rosca.fatias.length} partes. Total: ${centroValor}${centroRotulo && centroRotulo !== "Total" ? " " + centroRotulo : ""}.` : "Gráfico de rosca sem dados.")
681
+ };
682
+ }
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Densidade global de interação (decisão 25/07, simulação de adoção).
3
+ *
4
+ * O modo "touch" (`<html data-density="touch">`) eleva os alvos de toque dos
5
+ * controles interativos para ≥44px (Apple HIG / WCAG 2.5.8), aumentando
6
+ * controle e espaçamento juntos — sem hit-areas invisíveis, que roubam
7
+ * clique de vizinhos em listas densas. O default permanece idêntico.
8
+ */
9
+ export type Density = "default" | "touch";
10
+ const DENSITY_ATTRIBUTE = "data-density";
11
+
12
+ /** Define a densidade global (remove o atributo quando "default"). */
13
+ export const setDensity = (density: Density): void => {
14
+ if (typeof document === "undefined") return;
15
+ if (density === "default") {
16
+ document.documentElement.removeAttribute(DENSITY_ATTRIBUTE);
17
+ } else {
18
+ document.documentElement.setAttribute(DENSITY_ATTRIBUTE, density);
19
+ }
20
+ };
21
+
22
+ /** Densidade global atual. */
23
+ export const getDensity = (): Density => {
24
+ if (typeof document === "undefined") return "default";
25
+ const value = document.documentElement.getAttribute(DENSITY_ATTRIBUTE);
26
+ return value === "touch" ? "touch" : "default";
27
+ }