@aurea-uds/react 0.30.0 → 0.32.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/README.md +26 -0
- package/dist/actions.d.ts +33 -6
- package/dist/actions.js +38 -26
- package/dist/agents.js +1 -1
- package/dist/calendar.d.ts +38 -1
- package/dist/calendar.js +115 -2
- package/dist/code-client.d.ts +3 -1
- package/dist/code-client.js +23 -1
- package/dist/color.d.ts +92 -0
- package/dist/color.js +293 -0
- package/dist/disclosure.d.ts +19 -9
- package/dist/disclosure.js +8 -1
- package/dist/feedback-client.d.ts +11 -5
- package/dist/feedback-client.js +26 -15
- package/dist/graph-geometria.d.ts +84 -0
- package/dist/graph-geometria.js +437 -0
- package/dist/graph-worker.d.ts +1 -0
- package/dist/graph-worker.js +17 -0
- package/dist/graph.d.ts +72 -4
- package/dist/graph.js +1059 -166
- package/dist/icon-names.d.ts +7 -1
- package/dist/icon-names.js +2 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/inputs-client.d.ts +215 -2
- package/dist/inputs-client.js +504 -8
- package/dist/inputs.d.ts +1 -1
- package/dist/internal.d.ts +1 -0
- package/dist/internal.js +4 -0
- package/dist/layout-client.d.ts +14 -2
- package/dist/layout-client.js +2 -2
- package/dist/markup.d.ts +89 -9
- package/dist/markup.js +48 -11
- package/dist/navigation-client.d.ts +31 -7
- package/dist/navigation-client.js +60 -16
- package/dist/overlays.d.ts +20 -6
- package/dist/overlays.js +65 -10
- package/dist/pure.d.ts +42 -0
- package/dist/pure.js +2 -2
- package/dist/system.d.ts +5 -2
- package/dist/system.js +17 -9
- package/package.json +13 -3
package/dist/graph.js
CHANGED
|
@@ -39,11 +39,12 @@ import { Fragment as _Fragment, jsx as _jsx, jsxs as _jsxs } from "react/jsx-run
|
|
|
39
39
|
// Quem importa a folha é o CONSUMIDOR, uma vez, como já importa o `@aurea-uds/core/css`: um
|
|
40
40
|
// `import "…css"` dentro deste módulo quebraria em Node puro e o build daqui é `tsc` só.
|
|
41
41
|
import React from "react";
|
|
42
|
-
import { ReactFlow, ReactFlowProvider, Background, BaseEdge, EdgeLabelRenderer, Handle, MiniMap, Panel, Position, applyNodeChanges, getBezierPath, getSmoothStepPath, useReactFlow } from "@xyflow/react";
|
|
42
|
+
import { ReactFlow, ReactFlowProvider, Background, BaseEdge, EdgeLabelRenderer, EdgeText, Handle, MiniMap, Panel, Position, SelectionMode, applyNodeChanges, getBezierPath, getSmoothStepPath, useNodesInitialized, useOnViewportChange, useReactFlow, useStore } from "@xyflow/react";
|
|
43
43
|
import { cx, useAureaStrings } from "./internal.js";
|
|
44
44
|
import { Icon } from "./system.js";
|
|
45
45
|
import { Badge } from "./markup.js";
|
|
46
|
-
import { IconButton } from "./actions.js";
|
|
46
|
+
import { IconButton, Toolbar, ToolbarButton, ToolbarSeparator } from "./actions.js";
|
|
47
|
+
import { alinhar, distribuir, subarvores, recolher, remapear, simplificar, pontes, tracado, meioDoTracado, ladoDoTrecho, rotaCruza, bordaNaDirecao, rotulosQueSomem, contornoDeNuvem } from "./graph-geometria.js";
|
|
47
48
|
// ── O layout ─────────────────────────────────────────────────────────────────────────────
|
|
48
49
|
// O motor recebe `x`/`y` PRONTOS: ele não posiciona nada. Sem isto, um grafo sem coordenadas
|
|
49
50
|
// empilha tudo em (0,0) — que foi a medição que quase derrubou a escolha do motor.
|
|
@@ -56,6 +57,25 @@ import { IconButton } from "./actions.js";
|
|
|
56
57
|
// subida é uma biblioteca de layout automático de grafos, a que a referência usa, e ela é DEPENDÊNCIA NOVA: entra com
|
|
57
58
|
// autorização, não de contrabando.
|
|
58
59
|
const LARGURA = 180, ALTURA = 52, VAO_X = 90, VAO_Y = 24;
|
|
60
|
+
// ── AS MEDIDAS DA RODADA 2 (ADR-0065), cada uma com o token que dá o mesmo número ─────────────────
|
|
61
|
+
// O contêiner: a cabeça (botão de fechar + nome) tem `--space-10` (40) e o recheio em volta dos
|
|
62
|
+
// filhos é `--space-4` (16) — a caixa desenhada e o recheio que o arrumador usa são os MESMOS números.
|
|
63
|
+
const CABECA_DO_GRUPO = 40, RECHEIO_DO_GRUPO = 16;
|
|
64
|
+
// A grade do modo de arrumar: `--space-4` (16). As setas andam um passo dela; com Shift, quatro.
|
|
65
|
+
const GRADE = 16;
|
|
66
|
+
// A ponte no cruzamento: meia elipse de raio `--radius-xs` (6) — o tamanho do salto do draw.io.
|
|
67
|
+
const RAIO_DA_PONTE = 6;
|
|
68
|
+
// O desvio de linhas (`@tisoap/react-flow-smart-edge`): folga em volta do nó e malha da busca, as duas
|
|
69
|
+
// `--space-2` (8). Com folga de 16, o vão de 24 entre dois irmãos (`VAO_Y`) fechava de vez, e a linha
|
|
70
|
+
// entre dois vizinhos dava a volta no mapa inteiro (medido na bancada, na arrumação em árvore).
|
|
71
|
+
const FOLGA_DO_DESVIO = 8, MALHA_DO_DESVIO = 8;
|
|
72
|
+
// Abaixo desta escala o texto das linhas some (MNT-12.5 e 19.1): a 60%, o `--text-xs` vira ~8px.
|
|
73
|
+
const ESCALA_DOS_ROTULOS = 0.6;
|
|
74
|
+
// O mapa grande (MNT-19.1) precisa caber na tela: o motor para em 50% por padrão, e um site de 100
|
|
75
|
+
// equipamentos com andares não cabe nisso. Afasta até 10%.
|
|
76
|
+
// A margem que o motor põe em volta do mapa ao caber na tela (o padrão do `fitView` dele, 10%).
|
|
77
|
+
const MARGEM_DO_MOTOR = 0.1;
|
|
78
|
+
const ESCALA_MINIMA = 0.1;
|
|
59
79
|
// O VÃO ENTRE COLUNAS CRESCE COM O RÓTULO (A-09, 23/09/2026). Com o rótulo legível, a primeira
|
|
60
80
|
// imagem mostrou o que o preto escondia: "Te1/1/1 ↔ Te1/0/1" é mais largo que 90px e entrava por
|
|
61
81
|
// baixo dos nós. O layout roda também no servidor, onde não há texto para medir, então a largura
|
|
@@ -142,53 +162,132 @@ function dispor(nodes, edges, orientacao = "horizontal", m = { larg: LARGURA, al
|
|
|
142
162
|
// roda depois do primeiro desenho: até a resposta, e no servidor, vale a arrumação simples acima.
|
|
143
163
|
// Sem o pacote instalado, o mapa avisa uma vez no console e fica na simples: não quebra a tela.
|
|
144
164
|
const LADO_ELK = { top: "NORTH", right: "EAST", bottom: "SOUTH", left: "WEST" };
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const volta = [...do_("top"), ...do_("right"), ...do_("bottom").reverse(), ...do_("left").reverse()];
|
|
152
|
-
return volta.map((p, i) => [p, i]);
|
|
153
|
-
}
|
|
165
|
+
// ⚠ 10/10/2026, rodada 1: a porta ia com ORDEM FIXA (`FIXED_ORDER`), e não só com o lado — o arrumador
|
|
166
|
+
// livre reordenava as portas por dentro e a "porta 1" à esquerda ia ao vizinho da direita (as linhas se
|
|
167
|
+
// cruzavam, medido na bancada). Na rodada 2 a ordem virou POSIÇÃO FIXA (`FIXED_POS`): a rota que o
|
|
168
|
+
// arrumador devolve tem de nascer EXATAMENTE na alça desenhada, e a alça fica na fração do lado.
|
|
169
|
+
// E no nó com porta, a linha SEM porta vai a uma porta "de base", no meio do lado de entrada ou de
|
|
170
|
+
// saída: medido no protótipo, sem ela o arrumador ligava a linha no CANTO do nó (o 0,0 da caixa).
|
|
154
171
|
let avisouSemElk = false;
|
|
155
|
-
|
|
156
|
-
|
|
172
|
+
let elkNaTela = null;
|
|
173
|
+
// FORA DA TELA PRINCIPAL (MNT-19.1): o `elk.bundled.js` roda na própria tela (é um "falso trabalhador").
|
|
174
|
+
// Com `layoutWorker`, o APP entrega o trabalhador de verdade — o jeito de criar um trabalhador muda de
|
|
175
|
+
// empacotador para empacotador, e copiar o do `elkjs` para dentro do nosso pacote seria redistribuí-lo
|
|
176
|
+
// (decisão do Victor, 10/10/2026). A receita de cada empacotador está no README.
|
|
177
|
+
async function motorElk(trabalhador) {
|
|
157
178
|
try {
|
|
158
|
-
|
|
179
|
+
if (trabalhador) {
|
|
180
|
+
const ELK = (await import("elkjs/lib/elk-api.js")).default;
|
|
181
|
+
return new ELK({ workerFactory: () => trabalhador() });
|
|
182
|
+
}
|
|
183
|
+
elkNaTela ??= import("elkjs/lib/elk.bundled.js").then(m => new m.default());
|
|
184
|
+
return await elkNaTela;
|
|
159
185
|
}
|
|
160
186
|
catch {
|
|
187
|
+
elkNaTela = null;
|
|
161
188
|
if (!avisouSemElk) {
|
|
162
189
|
avisouSemElk = true;
|
|
163
|
-
console.warn("DependencyGraph: layout=\"layered\"
|
|
190
|
+
console.warn("DependencyGraph: layout=\"layered\", \"tree\" e \"radial\" precisam do pacote `elkjs` (peer opcional). Ficou a arrumação simples.");
|
|
164
191
|
}
|
|
165
192
|
return null;
|
|
166
193
|
}
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
194
|
+
}
|
|
195
|
+
const PORTA_DE_ENTRADA = "\u0000entra", PORTA_DE_SAIDA = "\u0000sai";
|
|
196
|
+
async function arrumarComElk(elk, e) {
|
|
197
|
+
const vertical = e.orientacao === "vertical";
|
|
198
|
+
const hierarquia = e.algoritmo === "layered" && e.grupos.length > 0;
|
|
199
|
+
const ids = new Set(e.folhas.map(f => f.id)), grupos = new Set(hierarquia ? e.grupos.map(g => g.id) : []);
|
|
200
|
+
const existe = (x) => ids.has(x) || grupos.has(x);
|
|
201
|
+
let arestas = e.arestas.filter(a => existe(a.de) && existe(a.para));
|
|
202
|
+
if (e.algoritmo !== "layered") {
|
|
203
|
+
// A árvore geradora: a primeira linha que alcança cada nó, em largura, a partir da raiz.
|
|
204
|
+
const raizes = new Set([...e.folhas.filter(f => f.raiz).map(f => f.id), ...e.folhas.filter(f => !arestas.some(a => a.para === f.id)).map(f => f.id)]);
|
|
205
|
+
const visto = new Set(raizes), fila = [...raizes], arvore = [];
|
|
206
|
+
while (fila.length || visto.size < ids.size) {
|
|
207
|
+
if (!fila.length) {
|
|
208
|
+
const solto = e.folhas.find(f => !visto.has(f.id));
|
|
209
|
+
visto.add(solto.id);
|
|
210
|
+
fila.push(solto.id);
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const n = fila.shift();
|
|
214
|
+
for (const a of arestas) {
|
|
215
|
+
const outro = a.de === n ? a.para : a.para === n ? a.de : null;
|
|
216
|
+
if (outro && ids.has(outro) && !visto.has(outro)) {
|
|
217
|
+
visto.add(outro);
|
|
218
|
+
fila.push(outro);
|
|
219
|
+
arvore.push({ ...a, de: n, para: outro, portaDe: undefined, portaPara: undefined });
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
arestas = arvore;
|
|
224
|
+
}
|
|
225
|
+
const comPortas = new Set(e.algoritmo === "layered" ? e.folhas.filter(f => f.portas.length).map(f => f.id) : []);
|
|
226
|
+
const noElk = (f) => {
|
|
227
|
+
const opcoes = {};
|
|
228
|
+
if (f.raiz && e.algoritmo === "layered")
|
|
229
|
+
opcoes["elk.layered.layering.layerConstraint"] = "FIRST";
|
|
230
|
+
if (f.camada != null && e.algoritmo === "layered")
|
|
231
|
+
opcoes["elk.layered.layering.layerChoiceConstraint"] = String(f.camada);
|
|
232
|
+
if (!comPortas.has(f.id))
|
|
233
|
+
return { id: f.id, width: f.larg, height: f.alt, layoutOptions: opcoes };
|
|
234
|
+
opcoes["elk.portConstraints"] = "FIXED_POS";
|
|
235
|
+
const porLado = new Map();
|
|
236
|
+
for (const p of f.portas)
|
|
237
|
+
porLado.set(p.side, [...(porLado.get(p.side) ?? []), p]);
|
|
238
|
+
const xy = (s, frac) => s === "left" ? { x: 0, y: f.alt * frac } : s === "right" ? { x: f.larg, y: f.alt * frac } : s === "top" ? { x: f.larg * frac, y: 0 } : { x: f.larg * frac, y: f.alt };
|
|
239
|
+
const porta = (id, s, frac) => ({ id: `${f.id}::${id}`, width: 0, height: 0, ...xy(s, frac), layoutOptions: { "elk.port.side": LADO_ELK[s] } });
|
|
240
|
+
const entra = vertical ? "top" : "left", sai = vertical ? "bottom" : "right";
|
|
241
|
+
return { id: f.id, width: f.larg, height: f.alt, layoutOptions: opcoes,
|
|
242
|
+
ports: [...[...porLado].flatMap(([s, l]) => l.map((p, k) => porta(p.id, s, (k + 1) / (l.length + 1)))), porta(PORTA_DE_ENTRADA, entra, 1 / 2), porta(PORTA_DE_SAIDA, sai, 1 / 2)] };
|
|
243
|
+
};
|
|
244
|
+
const portasValidas = new Set(e.folhas.flatMap(f => f.portas.map(p => `${f.id}::${p.id}`)));
|
|
245
|
+
const ponta = (no, porta, saida) => !comPortas.has(no) ? no
|
|
246
|
+
: porta && portasValidas.has(`${no}::${porta}`) ? `${no}::${porta}` : `${no}::${saida ? PORTA_DE_SAIDA : PORTA_DE_ENTRADA}`;
|
|
247
|
+
// Cada folha DENTRO do contêiner dela. O recheio do contêiner é o mesmo da caixa desenhada.
|
|
248
|
+
const raiz = { id: "\u0000raiz", children: [], edges: [] };
|
|
249
|
+
const caixas = new Map();
|
|
250
|
+
const recheio = `[top=${RECHEIO_DO_GRUPO + CABECA_DO_GRUPO},left=${RECHEIO_DO_GRUPO},bottom=${RECHEIO_DO_GRUPO},right=${RECHEIO_DO_GRUPO}]`;
|
|
251
|
+
if (hierarquia)
|
|
252
|
+
for (const g of e.grupos)
|
|
253
|
+
caixas.set(g.id, { id: g.id, children: [], layoutOptions: { "elk.padding": recheio } });
|
|
254
|
+
const dentro = (pai) => (pai && caixas.get(pai)) || raiz;
|
|
255
|
+
if (hierarquia)
|
|
256
|
+
for (const g of e.grupos)
|
|
257
|
+
dentro(g.pai).children.push(caixas.get(g.id));
|
|
258
|
+
// A raiz pedida vai PRIMEIRO: o círculo e a árvore começam pelo primeiro nó.
|
|
259
|
+
for (const f of [...e.folhas].sort((a, b) => Number(!!b.raiz) - Number(!!a.raiz)))
|
|
260
|
+
dentro(hierarquia ? f.pai : undefined).children.push(noElk(f));
|
|
261
|
+
raiz.edges = arestas.map(a => ({ id: a.id, sources: [ponta(a.de, a.portaDe, true)], targets: [ponta(a.para, a.portaPara, false)] }));
|
|
262
|
+
raiz.layoutOptions = { "elk.json.shapeCoords": "ROOT", "elk.json.edgeCoords": "ROOT", "elk.spacing.nodeNode": String(VAO_Y),
|
|
263
|
+
...(e.algoritmo === "layered" ? { "elk.algorithm": "layered", "elk.direction": vertical ? "DOWN" : "RIGHT",
|
|
175
264
|
"elk.layered.spacing.nodeNodeBetweenLayers": String(VAO_X),
|
|
176
|
-
"elk.
|
|
177
|
-
"elk.
|
|
178
|
-
"elk.
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
265
|
+
"elk.layered.nodePlacement.strategy": "BRANDES_KOEPF", "elk.layered.nodePlacement.bk.fixedAlignment": "BALANCED",
|
|
266
|
+
...(hierarquia ? { "elk.hierarchyHandling": "INCLUDE_CHILDREN" } : {}) }
|
|
267
|
+
: e.algoritmo === "mrtree" ? { "elk.algorithm": "mrtree", "elk.direction": vertical ? "DOWN" : "RIGHT" }
|
|
268
|
+
: { "elk.algorithm": "radial" }) };
|
|
269
|
+
let r;
|
|
270
|
+
try {
|
|
271
|
+
r = await elk.layout(raiz);
|
|
272
|
+
}
|
|
273
|
+
catch (erro) {
|
|
274
|
+
console.warn("DependencyGraph: o arrumador recusou o mapa; ficou a arrumação simples.", erro);
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
277
|
+
const posicoes = new Map(), rotas = new Map();
|
|
278
|
+
const percorrer = (n) => {
|
|
279
|
+
for (const c of n.children ?? []) {
|
|
280
|
+
posicoes.set(c.id, { x: c.x ?? 0, y: c.y ?? 0 });
|
|
281
|
+
percorrer(c);
|
|
282
|
+
}
|
|
283
|
+
// A rota só vale no `layered`: a árvore devolve diagonais tortas, e o círculo, retas que o mapa já traça.
|
|
284
|
+
if (e.algoritmo === "layered")
|
|
285
|
+
for (const a of (n.edges ?? []))
|
|
286
|
+
if (a.sections?.length)
|
|
287
|
+
rotas.set(a.id, simplificar(a.sections.flatMap(s => [s.startPoint, ...(s.bendPoints ?? []), s.endPoint])));
|
|
288
|
+
};
|
|
289
|
+
percorrer(r);
|
|
290
|
+
return { posicoes, rotas };
|
|
192
291
|
}
|
|
193
292
|
// ── AS ALÇAS: o lado de sempre, e as portas (MNT-06 e MNT-14) ─────────────────────────────────
|
|
194
293
|
// Sem porta, a linha entra por um lado e sai pelo oposto: esquerda→direita no horizontal, cima→
|
|
@@ -214,8 +313,20 @@ function alcasDoNo(n, orientacao, m) {
|
|
|
214
313
|
});
|
|
215
314
|
return [...base, ...portas];
|
|
216
315
|
}
|
|
316
|
+
/** Onde a alça fica NO MAPA, com a caixa medida (a fração do lado vale para qualquer tamanho). */
|
|
317
|
+
const pontoDaAlca = (c, a) => a.position === Position.Left ? { x: c.x, y: c.y + c.alt * a.frac } : a.position === Position.Right ? { x: c.x + c.larg, y: c.y + c.alt * a.frac }
|
|
318
|
+
: a.position === Position.Top ? { x: c.x + c.larg * a.frac, y: c.y } : { x: c.x + c.larg * a.frac, y: c.y + c.alt };
|
|
217
319
|
const SETAS = new Set(["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"]);
|
|
218
|
-
|
|
320
|
+
/**
|
|
321
|
+
* O botão de fechar e abrir (MNT-16.4 e 16.7), no canto de baixo à direita — fora do nome, longe do
|
|
322
|
+
* selo de estado (embaixo, à esquerda) e da alça (no meio do lado). Fechado, ele mostra QUANTOS o nó
|
|
323
|
+
* guarda ("+12"), o "número no canto" do mercado; aberto, um traço. O nome diz o que ele faz.
|
|
324
|
+
*/
|
|
325
|
+
// O clique NÃO sobe até a caixa do motor: no modo de arrumar, ela escolheria o nó junto (medido na bancada).
|
|
326
|
+
function BotaoDeRecolher({ r, rotulo }) {
|
|
327
|
+
return _jsx("button", { type: "button", className: "graph-node-toggle nodrag nopan", "aria-expanded": !r.fechado, "aria-label": `${r.fechado ? r.abrir : r.fechar} ${rotulo}${r.fechado ? ` (${r.n} ${r.ocultos})` : ""}`, onClick: e => { e.stopPropagation(); r.alternar(); }, children: r.fechado ? _jsxs("span", { "aria-hidden": "true", children: ["+", r.n] }) : _jsx(Icon, { name: "minus", size: "sm" }) });
|
|
328
|
+
}
|
|
329
|
+
const NoAurea = React.memo(function NoAurea({ data, selected, width, height }) {
|
|
219
330
|
const d = data;
|
|
220
331
|
const corpo = _jsxs(_Fragment, { children: [d.icone && _jsx(Icon, { name: d.icone, size: "md", className: "graph-node-icon" }), _jsxs("span", { className: "graph-node-text", children: [_jsx("span", { className: "graph-node-label", children: d.label }), d.kind && _jsx("span", { className: "graph-node-kind", children: d.kind }), d.detail && _jsx("span", { className: "hint", children: d.detail })] })] });
|
|
221
332
|
// As SETAS andam de nó em nó (MNT-19.4) — o mais perto na direção da seta. Só no nó que é botão:
|
|
@@ -224,15 +335,21 @@ function NoAurea({ data }) {
|
|
|
224
335
|
e.preventDefault();
|
|
225
336
|
d.aoTeclar(e.key);
|
|
226
337
|
} };
|
|
227
|
-
|
|
338
|
+
// A NUVEM (MNT-16.5) é desenho de FUNDO, do tamanho medido do nó: recortar a caixa cortaria o foco.
|
|
339
|
+
const larg = width ?? LARGURA, alt = height ?? ALTURA;
|
|
340
|
+
return _jsxs("div", { className: "graph-node", "data-selected": d.selecionado || selected || undefined, "data-focused": d.focado || undefined, "data-dimmed": d.apagado || undefined, "data-shape": d.nuvem ? "cloud" : undefined, children: [d.alcas.map(a => {
|
|
228
341
|
// A alça de porta fica no ponto dela ao longo do lado, em porcentagem: o nó pode crescer.
|
|
229
342
|
const lugar = a.position === Position.Left || a.position === Position.Right ? { top: `${a.frac * 100}%` } : { left: `${a.frac * 100}%` };
|
|
230
343
|
return _jsx(Handle, { id: a.id, type: a.type, position: a.position, isConnectable: !!d.conectavel, className: cx("graph-handle", !d.conectavel && "graph-handle-oculta"), style: a.id ? lugar : undefined, title: d.conectavel ? a.rotulo : undefined }, `${a.type}:${a.id ?? "lado"}`);
|
|
231
|
-
}), d.aoSelecionar
|
|
344
|
+
}), d.nuvem && _jsx("svg", { className: "graph-node-cloud", viewBox: `0 0 ${larg} ${alt}`, "aria-hidden": "true", children: _jsx("path", { d: contornoDeNuvem(larg, alt) }) }), d.aoSelecionar && !d.editavel
|
|
232
345
|
? _jsx("button", { type: "button", className: cx("graph-node-body", d.icone && "graph-node-has-icon"), "aria-pressed": !!d.selecionado, onClick: d.aoSelecionar, onKeyDown: teclar, children: corpo })
|
|
233
|
-
: _jsx("span", { className: cx("graph-node-body", d.icone && "graph-node-has-icon"), children: corpo }), d.contagem != null && _jsxs("span", { className: "graph-node-count", children: [_jsx(Badge, { size: "xs", variant: "neutral", emphasis: "solid", "aria-hidden": d.nomeDaContagem ? true : undefined, children: d.contagem }), d.nomeDaContagem && _jsx("span", { className: "sr-only", children: d.nomeDaContagem })] }), d.estado && _jsx("span", { className: "graph-node-status", children: _jsx(Badge, { size: "xs", variant: d.estado.tone ?? "neutral", children: d.estado.label }) })] });
|
|
234
|
-
}
|
|
235
|
-
const
|
|
346
|
+
: _jsx("span", { className: cx("graph-node-body", d.icone && "graph-node-has-icon"), children: corpo }), d.contagem != null && _jsxs("span", { className: "graph-node-count", children: [_jsx(Badge, { size: "xs", variant: "neutral", emphasis: "solid", "aria-hidden": d.nomeDaContagem ? true : undefined, children: d.contagem }), d.nomeDaContagem && _jsx("span", { className: "sr-only", children: d.nomeDaContagem })] }), d.estado && _jsx("span", { className: "graph-node-status", children: _jsx(Badge, { size: "xs", variant: d.estado.tone ?? "neutral", children: d.estado.label }) }), d.fixo && _jsxs("span", { className: "graph-node-pin", children: [_jsx(Icon, { name: "push-pin", size: "sm" }), _jsx("span", { className: "sr-only", children: d.fixo.nome })] }), d.recolher && _jsx(BotaoDeRecolher, { r: d.recolher, rotulo: d.label })] });
|
|
347
|
+
});
|
|
348
|
+
const NoGrupo = React.memo(function NoGrupo({ data, selected }) {
|
|
349
|
+
const d = data;
|
|
350
|
+
return _jsxs("div", { className: "graph-group", "data-selected": selected || undefined, "data-dimmed": d.apagado || undefined, children: [d.alcas.map(a => _jsx(Handle, { type: a.type, position: a.position, isConnectable: false, className: "graph-handle graph-handle-oculta" }, `${a.type}:lado`)), _jsxs("div", { className: "graph-group-head", children: [_jsx("button", { type: "button", className: "graph-group-toggle nodrag nopan", "aria-expanded": "true", "aria-label": `${d.recolher.fechar} ${d.label}`, onClick: e => { e.stopPropagation(); d.recolher.alternar(); }, children: _jsx(Icon, { name: "caret-down", size: "sm" }) }), d.icone && _jsx(Icon, { name: d.icone, size: "sm", className: "graph-node-icon" }), _jsx("span", { className: "graph-group-label", children: d.label }), d.kind && _jsx("span", { className: "graph-node-kind", children: d.kind }), d.fixo && _jsxs("span", { className: "graph-node-pin", children: [_jsx(Icon, { name: "push-pin", size: "sm" }), _jsx("span", { className: "sr-only", children: d.fixo.nome })] })] })] });
|
|
351
|
+
});
|
|
352
|
+
const TIPOS = { aurea: NoAurea, aureaGrupo: NoGrupo };
|
|
236
353
|
// ── A ARESTA, e por que ela também é nossa (A-09 e A-10, 23/09/2026) ─────────────────────────
|
|
237
354
|
// A-09 · O RÓTULO SAÍA PRETO SOBRE PRETO no tema escuro. O core declara `--xy-edge-label-color` e
|
|
238
355
|
// `--xy-edge-label-background-color` desde o H14, e NINGUÉM AS LIA: quem consome essas duas é o
|
|
@@ -261,21 +378,50 @@ const pontaDoRotulo = (x, y, pos) => {
|
|
|
261
378
|
return `translate(${x + f}px,${y - f}px) translateY(-100%)`;
|
|
262
379
|
return `translate(${x - f}px,${y - f}px) translate(-100%,-100%)`;
|
|
263
380
|
};
|
|
264
|
-
function ArestaAurea({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, label, data, markerEnd, style }) {
|
|
381
|
+
const ArestaAurea = React.memo(function ArestaAurea({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, label, data, markerEnd, style }) {
|
|
265
382
|
const d = (data ?? { deslocamento: 0, vertical: false, forma: "curve" });
|
|
266
383
|
// A-10 · as paralelas se afastam na direção ATRAVESSADA à linha: na vertical, para os lados.
|
|
267
384
|
const dx = d.vertical ? d.deslocamento : 0, dy = d.vertical ? 0 : d.deslocamento;
|
|
268
|
-
|
|
269
|
-
|
|
385
|
+
// COM ROTA (MNT-12.1 e 12.3), o traçado é NOSSO: passa pelos pontos, dobra com o canto da casa e salta
|
|
386
|
+
// nas pontes. A ponta e o lado de cada rótulo da ponta saem do primeiro e do último trecho dela — no
|
|
387
|
+
// arrumador, a linha nasce onde ele a repartiu no lado, e não no meio da alça.
|
|
388
|
+
const rota = d.rota && d.rota.length > 1 ? d.rota : undefined;
|
|
389
|
+
let caminho, lx, ly, ini, fim, ladoIni, ladoFim;
|
|
390
|
+
if (rota) {
|
|
391
|
+
caminho = tracado(rota, CANTO_DO_DEGRAU, d.saltos ? new Map(d.saltos) : undefined, RAIO_DA_PONTE);
|
|
392
|
+
({ x: lx, y: ly } = meioDoTracado(rota));
|
|
393
|
+
ini = rota[0];
|
|
394
|
+
fim = rota[rota.length - 1];
|
|
395
|
+
ladoIni = LADO[ladoDoTrecho(rota[0], rota[1], true)];
|
|
396
|
+
ladoFim = LADO[ladoDoTrecho(rota[rota.length - 2], fim, false)];
|
|
397
|
+
}
|
|
398
|
+
else {
|
|
399
|
+
const pontos = { sourceX: sourceX + dx, sourceY: sourceY + dy, sourcePosition, targetX: targetX + dx, targetY: targetY + dy, targetPosition };
|
|
400
|
+
[caminho, lx, ly] = d.forma === "step" ? getSmoothStepPath({ ...pontos, borderRadius: CANTO_DO_DEGRAU }) : getBezierPath(pontos);
|
|
401
|
+
ini = { x: pontos.sourceX, y: pontos.sourceY };
|
|
402
|
+
fim = { x: pontos.targetX, y: pontos.targetY };
|
|
403
|
+
ladoIni = sourcePosition;
|
|
404
|
+
ladoFim = targetPosition;
|
|
405
|
+
}
|
|
270
406
|
// O rótulo de texto é SVG (sai também no servidor). Quando a linha tem marca, número ou grupo, o
|
|
271
407
|
// rótulo vai para a pastilha de HTML do meio, junto deles — dois desenhos no mesmo ponto se cobririam.
|
|
272
408
|
// O traço DUPLO desenha o miolo por cima da linha — e por cima do rótulo SVG, que sairia riscado
|
|
273
409
|
// (medido na bancada). Com rótulo, ele também vai para a pastilha.
|
|
274
|
-
const pastilha = !!(d.marca || d.contagem || d.grupo || (d.padrao === "double" && label != null));
|
|
275
|
-
const cls = cx("graph-edge", d.padrao && d.padrao !== "solid" && `graph-edge-${d.padrao}`, d.peso && d.peso !== "regular" && `graph-edge-${d.peso}`, d.apagada && "is-dimmed");
|
|
276
|
-
|
|
277
|
-
}
|
|
410
|
+
const pastilha = !!(d.marca || d.marcaDoEstado || d.contagem || d.grupo || (d.padrao === "double" && label != null));
|
|
411
|
+
const cls = cx("graph-edge", d.padrao && d.padrao !== "solid" && `graph-edge-${d.padrao}`, d.peso && d.peso !== "regular" && `graph-edge-${d.peso}`, d.tom && d.tom !== "neutral" && `graph-edge-tone-${d.tom}`, d.apagada && "is-dimmed");
|
|
412
|
+
// MNT-12.5: cada rótulo leva quem é, a prioridade e a ordem — o mapa esconde o que encosta.
|
|
413
|
+
const marca = (parte) => ({ "data-edge-label": `${id}${parte}`, "data-priority": d.prioridade ?? 0, "data-order": d.ordem ?? 0 });
|
|
414
|
+
return _jsxs(_Fragment, { children: [_jsx(BaseEdge, { id: id, path: caminho, className: cls, markerEnd: markerEnd, style: style }), d.padrao === "double" && _jsx("path", { d: caminho, fill: "none", className: cx("graph-edge-double-core", d.peso && d.peso !== "regular" && `graph-edge-${d.peso}`, d.tom && d.tom !== "neutral" && `graph-edge-tone-${d.tom}`, d.apagada && "is-dimmed") }), !pastilha && label != null && _jsx(EdgeText, { x: lx, y: ly, label: label, labelStyle: ROTULO, labelShowBg: true, labelBgStyle: FUNDO_DO_ROTULO, className: cx("graph-edge-text", d.apagada && "is-dimmed"), ...marca("") }), (pastilha || d.origem || d.destino) && _jsxs(EdgeLabelRenderer, { children: [pastilha && _jsxs("div", { className: cx("graph-edge-chip nodrag nopan", d.apagada && "is-dimmed"), style: { transform: `translate(-50%,-50%) translate(${lx}px,${ly}px)` }, ...marca(""), children: [d.marca && _jsx(MarcaDaLinha, { marca: d.marca }), d.marcaDoEstado && d.marcaDoEstado !== d.marca && _jsx(MarcaDaLinha, { marca: d.marcaDoEstado }), label != null && _jsx("span", { className: "graph-edge-chip-text", children: label }), d.contagem != null && _jsxs("span", { className: "graph-edge-count", children: ["\u00D7", d.contagem] }), d.grupo && _jsxs("button", { type: "button", className: "graph-edge-group", "data-edge-group": id, "aria-expanded": d.grupo.aberto, "aria-label": `${d.grupo.aberto ? d.grupo.juntar : d.grupo.mostrar} ${d.grupo.n} ${d.grupo.conexoes}`, onClick: d.grupo.alternar, children: ["\u00D7", d.grupo.n] })] }), d.origem && _jsx("div", { className: cx("graph-edge-end", d.apagada && "is-dimmed"), style: { transform: pontaDoRotulo(ini.x, ini.y, ladoIni) }, ...marca(":origem"), children: d.origem }), d.destino && _jsx("div", { className: cx("graph-edge-end", d.apagada && "is-dimmed"), style: { transform: pontaDoRotulo(fim.x, fim.y, ladoFim) }, ...marca(":destino"), children: d.destino })] })] });
|
|
415
|
+
});
|
|
278
416
|
const TIPOS_DE_ARESTA = { aurea: ArestaAurea };
|
|
417
|
+
/** A marca do meio da linha: o × e o ponto são desenho do CSS; o cadeado, o ícone. */
|
|
418
|
+
function MarcaDaLinha({ marca }) {
|
|
419
|
+
return _jsx("span", { className: `graph-edge-mark graph-edge-mark-${marca}`, "aria-hidden": "true", children: marca === "lock" && _jsx(Icon, { name: "lock-key", size: "sm" }) });
|
|
420
|
+
}
|
|
421
|
+
/** A amostra de uma linha na legenda: o traço, o miolo do duplo e a marca — o mesmo desenho do mapa. */
|
|
422
|
+
function AmostraDaLinha({ padrao, peso, tom, marca }) {
|
|
423
|
+
return _jsxs(_Fragment, { children: [_jsxs("svg", { className: "graph-legend-sample", "aria-hidden": "true", viewBox: "0 0 32 8", children: [_jsx("path", { d: "M0 4H32", className: cx("graph-edge", padrao && padrao !== "solid" && `graph-edge-${padrao}`, peso && peso !== "regular" && `graph-edge-${peso}`, tom && tom !== "neutral" && `graph-edge-tone-${tom}`) }), padrao === "double" && _jsx("path", { d: "M0 4H32", className: cx("graph-edge-double-core", peso && peso !== "regular" && `graph-edge-${peso}`) })] }), marca && _jsx(MarcaDaLinha, { marca: marca })] });
|
|
424
|
+
}
|
|
279
425
|
/** A chave de um PAR sem sentido: o cabo entre A e B é o mesmo que entre B e A. */
|
|
280
426
|
const parSemSentido = (e) => e.from < e.to ? `${e.from}|${e.to}` : `${e.to}|${e.from}`;
|
|
281
427
|
/**
|
|
@@ -343,22 +489,29 @@ const EM_PAR = (a) => a ? new Set(a) : null;
|
|
|
343
489
|
const XML = (t) => t.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/\x22/g, """).replace(/\x27/g, "'");
|
|
344
490
|
const TRACO_DRAWIO = { solid: "", dashed: "dashed=1;dashPattern=8 4;", dotted: "dashed=1;dashPattern=1 4;", double: "shape=link;" };
|
|
345
491
|
const PESO_DRAWIO = { regular: 1, thick: 2, heavy: 3 };
|
|
346
|
-
|
|
492
|
+
// Rodada 2 (ADR-0065): o CONTÊINER sai como contêiner do draw.io (`container=1`), com os filhos dentro
|
|
493
|
+
// dele em coordenada RELATIVA — a mesma do motor —; a NUVEM sai com a forma de nuvem do draw.io; e a
|
|
494
|
+
// linha com rota leva as DOBRAS (`Array as="points"`), para o desenho abrir lá como está aqui.
|
|
495
|
+
function paraDrawio(nos, linhas, forma, titulo) {
|
|
347
496
|
const visiveis = nos.filter(n => !n.hidden);
|
|
348
497
|
const ids = new Set(visiveis.map(n => n.id));
|
|
349
498
|
const celulas = ['<mxCell id="0"/>', '<mxCell id="1" parent="0"/>'];
|
|
350
499
|
for (const n of visiveis) {
|
|
351
500
|
const d = n.data;
|
|
352
501
|
const texto = [d.label, d.kind, typeof d.detail === "string" ? d.detail : undefined].filter(Boolean).join("\n");
|
|
353
|
-
const w = n.measured?.width ?? n.initialWidth ?? LARGURA, h = n.measured?.height ?? n.initialHeight ?? ALTURA;
|
|
354
|
-
|
|
502
|
+
const w = n.width ?? n.measured?.width ?? n.initialWidth ?? LARGURA, h = n.height ?? n.measured?.height ?? n.initialHeight ?? ALTURA;
|
|
503
|
+
const estilo = n.type === "aureaGrupo" ? "rounded=1;arcSize=6;container=1;collapsible=0;verticalAlign=top;align=left;spacingLeft=8;whiteSpace=wrap;html=0;"
|
|
504
|
+
: d.nuvem ? "ellipse;shape=cloud;whiteSpace=wrap;html=0;" : "rounded=1;arcSize=20;whiteSpace=wrap;html=0;";
|
|
505
|
+
const pai = n.parentId && ids.has(n.parentId) ? XML(`n-${n.parentId}`) : "1";
|
|
506
|
+
celulas.push(`<mxCell id="${XML(`n-${n.id}`)}" value="${XML(texto)}" style="${estilo}" vertex="1" parent="${pai}"><mxGeometry x="${Math.round(n.position.x)}" y="${Math.round(n.position.y)}" width="${Math.round(w)}" height="${Math.round(h)}" as="geometry"/></mxCell>`);
|
|
355
507
|
}
|
|
356
|
-
|
|
508
|
+
linhas.forEach(({ item: e, rota }, i) => {
|
|
357
509
|
if (!ids.has(e.from) || !ids.has(e.to))
|
|
358
510
|
return;
|
|
359
511
|
const id = `e-${i}`;
|
|
360
512
|
const estilo = `endArrow=none;html=0;${forma === "step" ? "edgeStyle=orthogonalEdgeStyle;rounded=1;" : "curved=1;"}${TRACO_DRAWIO[e.pattern ?? "solid"]}strokeWidth=${PESO_DRAWIO[e.weight ?? "regular"]};`;
|
|
361
|
-
|
|
513
|
+
const dobras = rota && rota.length > 2 ? `<Array as="points">${rota.slice(1, -1).map(p => `<mxPoint x="${Math.round(p.x)}" y="${Math.round(p.y)}"/>`).join("")}</Array>` : "";
|
|
514
|
+
celulas.push(`<mxCell id="${id}" value="${XML(e.label ?? "")}" style="${estilo}" edge="1" parent="1" source="${XML(`n-${e.from}`)}" target="${XML(`n-${e.to}`)}"><mxGeometry relative="1" as="geometry">${dobras}</mxGeometry></mxCell>`);
|
|
362
515
|
// O nome da porta na ponta: o rótulo-filho da linha, a 80% do caminho para cada lado (o idioma do draw.io).
|
|
363
516
|
for (const [txt, x, suf] of [[e.sourceLabel, -0.8, "s"], [e.targetLabel, 0.8, "t"]])
|
|
364
517
|
if (txt)
|
|
@@ -366,29 +519,210 @@ function paraDrawio(nos, itens, forma, titulo) {
|
|
|
366
519
|
});
|
|
367
520
|
return `<mxfile host="Aurea"><diagram name="${XML(titulo)}" id="aurea"><mxGraphModel><root>${celulas.join("")}</root></mxGraphModel></diagram></mxfile>`;
|
|
368
521
|
}
|
|
369
|
-
|
|
522
|
+
// ── O DESVIO (MNT-12.1), peer OPCIONAL ──────────────────────────────────────────────────────────
|
|
523
|
+
// Depois de arrumar à mão, a rota do arrumador deixa de valer. A linha que passa POR DENTRO de um nó
|
|
524
|
+
// pede o desvio do `@tisoap/react-flow-smart-edge` (MIT, sem dependência; autorizado pelo Victor em
|
|
525
|
+
// 10/10/2026): só a CONTA dele — a função pura que devolve os pontos —, e não a aresta dele; o desenho
|
|
526
|
+
// continua o nosso, com o canto e as pontes. Roda em lotes pequenos entre um quadro e outro, para a
|
|
527
|
+
// tela não travar num mapa grande. Sem o pacote, a linha fica como está e o console avisa uma vez.
|
|
528
|
+
let desviadorCarregado = null;
|
|
529
|
+
let avisouSemDesvio = false;
|
|
530
|
+
function desviador() {
|
|
531
|
+
desviadorCarregado ??= import("@tisoap/react-flow-smart-edge").catch(() => {
|
|
532
|
+
if (!avisouSemDesvio) {
|
|
533
|
+
avisouSemDesvio = true;
|
|
534
|
+
console.warn("DependencyGraph: o desvio de linhas precisa do pacote `@tisoap/react-flow-smart-edge` (peer opcional). As linhas que cruzam nós ficaram como estão.");
|
|
535
|
+
}
|
|
536
|
+
return null;
|
|
537
|
+
});
|
|
538
|
+
return desviadorCarregado;
|
|
539
|
+
}
|
|
540
|
+
const LOTE_DO_DESVIO = 16;
|
|
541
|
+
/** O degrau simples, o mesmo do motor quando os nós estão bem postos: sai, dobra no meio, entra. */
|
|
542
|
+
function esqueleto(s, ls, t, lt) {
|
|
543
|
+
if (ls === Position.Bottom && lt === Position.Top && t.y > s.y) {
|
|
544
|
+
const my = (s.y + t.y) / 2;
|
|
545
|
+
return simplificar([s, { x: s.x, y: my }, { x: t.x, y: my }, t]);
|
|
546
|
+
}
|
|
547
|
+
if (ls === Position.Right && lt === Position.Left && t.x > s.x) {
|
|
548
|
+
const mx = (s.x + t.x) / 2;
|
|
549
|
+
return simplificar([s, { x: mx, y: s.y }, { x: mx, y: t.y }, t]);
|
|
550
|
+
}
|
|
551
|
+
return null;
|
|
552
|
+
}
|
|
553
|
+
const centroDe = (c) => ({ x: c.x + c.larg / 2, y: c.y + c.alt / 2 });
|
|
554
|
+
const naBorda = (p, c) => p.x >= c.x - 1 && p.x <= c.x + c.larg + 1 && p.y >= c.y - 1 && p.y <= c.y + c.alt + 1
|
|
555
|
+
&& (Math.abs(p.x - c.x) < 1 || Math.abs(p.x - c.x - c.larg) < 1 || Math.abs(p.y - c.y) < 1 || Math.abs(p.y - c.y - c.alt) < 1);
|
|
556
|
+
// O modo de arrumar: o laço é o botão principal na área vazia; a tela anda com o do meio e o direito
|
|
557
|
+
// (e com Espaço + arrastar, o padrão do motor). Shift, Ctrl e Cmd somam à escolha, como no draw.io.
|
|
558
|
+
const BOTOES_DE_ARRASTAR = [1, 2], TECLAS_DE_SOMAR = ["Shift", "Meta", "Control"];
|
|
559
|
+
// Esperar a medida chegar antes de chamar o arrumador: sem isto, ele roda duas vezes no começo.
|
|
560
|
+
const ESPERA_DO_ARRUMADOR = 30;
|
|
561
|
+
// O desfazer guarda os últimos CEM passos, como o draw.io. É PILHA DE COMANDOS (cada passo com o
|
|
562
|
+
// inverso), e não foto do mapa inteiro: o mapa é controlado pelo app, e cada passo — para frente ou
|
|
563
|
+
// para trás — vira um `onLayoutChange` com as posições, que o app guarda (decisão do Victor, 10/10/2026).
|
|
564
|
+
const LIMITE_DO_DESFAZER = 100;
|
|
565
|
+
export function DependencyGraph({ nodes, edges, label, selectedId, onSelect, connectable, onConnect, onNodeMove, height, className, orientation = "horizontal", layout = "simple", rootId, edgeShape = "curve", groupParallel, minimap, controls, legend, focusId, highlight, highlightNeighbors, hiddenIds, onNodeHover, onNodeContextMenu, onEdgeSelect, textSize = "md", highContrast, apiRef, editable, grid = true, onLayoutChange, onSelectionChange, collapsible, onCollapseChange, visibleOnly, layoutWorker, style, ...props }) {
|
|
370
566
|
const s = useAureaStrings();
|
|
371
567
|
const m = React.useMemo(() => medidasDoNo(textSize), [textSize]);
|
|
372
568
|
const vertical = orientation === "vertical";
|
|
569
|
+
const editavel = !!editable;
|
|
570
|
+
const porId = React.useMemo(() => new Map(nodes.map(n => [n.id, n])), [nodes]);
|
|
571
|
+
// ── O CONTÊINER (MNT-16.6): quem está dentro de quem ──────────────────────────────────────
|
|
572
|
+
const pais = React.useMemo(() => {
|
|
573
|
+
const p = new Map();
|
|
574
|
+
for (const n of nodes)
|
|
575
|
+
if (n.parentId && n.parentId !== n.id && porId.has(n.parentId))
|
|
576
|
+
p.set(n.id, n.parentId);
|
|
577
|
+
// Contêiner em ciclo (A dentro de B dentro de A) é defeito do dado; o elo que fecha o ciclo sai.
|
|
578
|
+
for (const id of [...p.keys()]) {
|
|
579
|
+
const visto = new Set([id]);
|
|
580
|
+
for (let q = p.get(id); q; q = p.get(q)) {
|
|
581
|
+
if (visto.has(q)) {
|
|
582
|
+
p.delete(id);
|
|
583
|
+
break;
|
|
584
|
+
}
|
|
585
|
+
visto.add(q);
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
return p;
|
|
589
|
+
}, [nodes, porId]);
|
|
590
|
+
const contemFilhos = React.useMemo(() => new Set(pais.values()), [pais]);
|
|
591
|
+
// ── O QUE O APP DISSE × O QUE A PESSOA FEZ (MNT-13) ─────────────────────────────────────────
|
|
592
|
+
// O mapa é controlado pelo app (`x`/`y`, `pinned`, `collapsed`), mas a pessoa mexe nele antes de o app
|
|
593
|
+
// guardar. Vale o ÚLTIMO QUE ESCREVEU: o que a pessoa fez vale até o app mudar AQUELE valor — aí vale
|
|
594
|
+
// o do app. Quem devolve o que recebeu pelo `onLayoutChange` não muda nada.
|
|
595
|
+
const [posMao, setPosMao] = React.useState(() => new Map());
|
|
596
|
+
const [fixoMao, setFixoMao] = React.useState(() => new Map());
|
|
597
|
+
const [fechadoMao, setFechadoMao] = React.useState(() => new Map());
|
|
598
|
+
const nodesAntes = React.useRef(nodes);
|
|
599
|
+
React.useEffect(() => {
|
|
600
|
+
const antes = new Map(nodesAntes.current.map(n => [n.id, n]));
|
|
601
|
+
nodesAntes.current = nodes;
|
|
602
|
+
const mudou = (f) => nodes.filter(n => { const a = antes.get(n.id); return !!a && f(a, n); }).map(n => n.id);
|
|
603
|
+
const sem = (ids) => (x) => { if (!ids.some(i => x.has(i)))
|
|
604
|
+
return x; const b = new Map(x); for (const i of ids)
|
|
605
|
+
b.delete(i); return b; };
|
|
606
|
+
setPosMao(sem(mudou((a, n) => a.x !== n.x || a.y !== n.y)));
|
|
607
|
+
setFixoMao(sem(mudou((a, n) => a.pinned !== n.pinned)));
|
|
608
|
+
setFechadoMao(sem(mudou((a, n) => a.collapsed !== n.collapsed)));
|
|
609
|
+
}, [nodes]);
|
|
610
|
+
const fechados = React.useMemo(() => new Set(nodes.filter(n => fechadoMao.get(n.id) ?? n.collapsed).map(n => n.id)), [nodes, fechadoMao]);
|
|
611
|
+
const fixos = React.useMemo(() => new Set(nodes.filter(n => fixoMao.get(n.id) ?? n.pinned).map(n => n.id)), [nodes, fixoMao]);
|
|
612
|
+
// ── O QUE FECHA (MNT-16.4, 16.6 e 16.7) ────────────────────────────────────────────────────
|
|
613
|
+
const dominados = React.useMemo(() => collapsible ? subarvores(nodes.filter(n => !contemFilhos.has(n.id)).map(n => n.id), edges, rootId) : new Map(), [collapsible, nodes, edges, rootId, contemFilhos]);
|
|
614
|
+
const { rep, guardados } = React.useMemo(() => recolher(nodes.map(n => n.id), pais, dominados, fechados, contemFilhos), [nodes, pais, dominados, fechados, contemFilhos]);
|
|
615
|
+
// ABERTO: o contêiner visível, não fechado, com algum filho visível. Todo o resto que se vê é FOLHA.
|
|
616
|
+
const abertosLogicos = React.useMemo(() => new Set([...contemFilhos].filter(c => rep.get(c) === c && !fechados.has(c)
|
|
617
|
+
&& nodes.some(n => pais.get(n.id) === c && rep.get(n.id) === n.id))), [contemFilhos, rep, fechados, nodes, pais]);
|
|
618
|
+
// Na ÁRVORE e no CÍRCULO o contêiner aberto não vira caixa: o arrumador só põe filho dentro de pai na
|
|
619
|
+
// arrumação em camadas, e a caixa em volta de filhos espalhados cobria os outros nós (medido na
|
|
620
|
+
// bancada). Ali os filhos aparecem soltos; fechado, o contêiner continua sendo um nó.
|
|
621
|
+
const abertos = React.useMemo(() => layout === "tree" || layout === "radial" ? new Set() : abertosLogicos, [layout, abertosLogicos]);
|
|
622
|
+
const folhas = React.useMemo(() => nodes.filter(n => rep.get(n.id) === n.id && !abertosLogicos.has(n.id)), [nodes, rep, abertosLogicos]);
|
|
623
|
+
const paiAberto = React.useCallback((id) => { const p = pais.get(id); return p && abertos.has(p) ? p : undefined; }, [pais, abertos]);
|
|
624
|
+
const profundidade = React.useCallback((id) => { let k = 0; for (let q = pais.get(id); q; q = pais.get(q))
|
|
625
|
+
k++; return k; }, [pais]);
|
|
626
|
+
const descendentes = React.useCallback((g) => folhas.filter(n => { for (let q = pais.get(n.id); q; q = pais.get(q))
|
|
627
|
+
if (q === g)
|
|
628
|
+
return true; return false; }).map(n => n.id), [folhas, pais]);
|
|
629
|
+
const desenhaveis = React.useMemo(() => new Set([...folhas.map(n => n.id), ...abertos]), [folhas, abertos]);
|
|
630
|
+
// As linhas depois do fechamento: a de dentro some, a do escondido sai do representante, e as que
|
|
631
|
+
// sobram entre o mesmo par viram UMA com o número. A porta do escondido não existe no representante.
|
|
632
|
+
const remapeadas = React.useMemo(() => remapear(edges, rep).map(({ item, de, para, n }) => de === item.from && para === item.to ? item : { ...item, from: de, to: para,
|
|
633
|
+
fromPort: de === item.from ? item.fromPort : undefined, toPort: para === item.to ? item.toPort : undefined,
|
|
634
|
+
sourceLabel: de === item.from ? item.sourceLabel : undefined, targetLabel: para === item.to ? item.targetLabel : undefined,
|
|
635
|
+
label: n > 1 ? undefined : item.label, count: n > 1 ? n : item.count })
|
|
636
|
+
// A linha que liga num contêiner sem caixa (árvore e círculo) não tem onde chegar: não se desenha.
|
|
637
|
+
.filter(e => desenhaveis.has(e.from) && desenhaveis.has(e.to)), [edges, rep, desenhaveis]);
|
|
638
|
+
const [gruposAbertos, setGruposAbertos] = React.useState(() => new Set());
|
|
639
|
+
const alternarGrupo = React.useCallback((k) => setGruposAbertos(a => { const b = new Set(a); if (b.has(k))
|
|
640
|
+
b.delete(k);
|
|
641
|
+
else
|
|
642
|
+
b.add(k); return b; }), []);
|
|
643
|
+
const desenhadas = React.useMemo(() => agruparParalelas(remapeadas, !!groupParallel, gruposAbertos), [remapeadas, groupParallel, gruposAbertos]);
|
|
644
|
+
const ident = React.useMemo(() => arestasParalelas(desenhadas.map(x => x.item), vertical ? m.larg : m.alt), [desenhadas, vertical, m]);
|
|
645
|
+
// ── AS MEDIDAS (vêm do motor; antes de medir, a estimada) ─────────────────────────────────
|
|
646
|
+
const [medidas, setMedidas] = React.useState(() => new Map());
|
|
647
|
+
const tamanho = React.useCallback((id) => medidas.get(id) ?? m, [medidas, m]);
|
|
648
|
+
// ── A ARRUMAÇÃO AUTOMÁTICA ─────────────────────────────────────────────────────────────────
|
|
373
649
|
// O layout só roda para quem não trouxe coordenada. Quem traz manda — é o que deixa a Parte I
|
|
374
650
|
// guardar a posição que a pessoa arrastou sem brigar com a disposição automática.
|
|
375
|
-
const automatico = React.useMemo(() => dispor(
|
|
376
|
-
|
|
377
|
-
const
|
|
651
|
+
const automatico = React.useMemo(() => dispor(folhas, remapeadas, orientation, m), [folhas, remapeadas, orientation, m]);
|
|
652
|
+
const algoritmo = layout === "layered" ? "layered" : layout === "tree" ? "mrtree" : layout === "radial" ? "radial" : null;
|
|
653
|
+
const entradaElk = React.useMemo(() => algoritmo ? { algoritmo, orientacao: orientation,
|
|
654
|
+
folhas: folhas.map(n => ({ id: n.id, ...tamanho(n.id), portas: n.ports ?? [], camada: n.layer, pai: paiAberto(n.id), raiz: n.id === rootId })),
|
|
655
|
+
grupos: [...abertos].sort((a, b) => profundidade(a) - profundidade(b)).map(id => ({ id, pai: paiAberto(id) })),
|
|
656
|
+
arestas: desenhadas.map((x, i) => ({ id: ident[i].id, de: x.item.from, para: x.item.to, portaDe: x.item.fromPort, portaPara: x.item.toPort })) } : null, [algoritmo, orientation, folhas, tamanho, paiAberto, rootId, abertos, profundidade, desenhadas, ident]);
|
|
657
|
+
const chaveElk = React.useMemo(() => entradaElk ? JSON.stringify(entradaElk) : "", [entradaElk]);
|
|
658
|
+
// A arrumação chega DEPOIS (é assíncrona e o pacote é opcional); até lá, e no servidor, a simples.
|
|
659
|
+
const [elk, setElk] = React.useState(null);
|
|
660
|
+
const trabalhador = React.useRef(layoutWorker);
|
|
661
|
+
trabalhador.current = layoutWorker;
|
|
662
|
+
const motorProprio = React.useRef(null);
|
|
663
|
+
React.useEffect(() => () => { motorProprio.current?.terminateWorker(); motorProprio.current = null; }, []);
|
|
664
|
+
// O mapa cabe na tela de novo quando chega a PRIMEIRA arrumação de um dado novo — e não quando um
|
|
665
|
+
// grupo abre ou fecha, que a câmera pular a cada clique desorienta.
|
|
666
|
+
const [pedidoDeCaber, setPedidoDeCaber] = React.useState(0);
|
|
667
|
+
const dadosCabidos = React.useRef(null);
|
|
378
668
|
React.useEffect(() => {
|
|
379
|
-
if (
|
|
380
|
-
|
|
669
|
+
if (!entradaElk) {
|
|
670
|
+
setElk(null);
|
|
381
671
|
return;
|
|
382
672
|
}
|
|
383
673
|
let vivo = true;
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
674
|
+
const espera = setTimeout(async () => {
|
|
675
|
+
let motor;
|
|
676
|
+
if (trabalhador.current) {
|
|
677
|
+
motorProprio.current ??= await motorElk(() => trabalhador.current());
|
|
678
|
+
motor = motorProprio.current;
|
|
679
|
+
}
|
|
680
|
+
else
|
|
681
|
+
motor = await motorElk();
|
|
682
|
+
if (!motor || !vivo)
|
|
683
|
+
return;
|
|
684
|
+
const r = await arrumarComElk(motor, entradaElk);
|
|
685
|
+
if (!vivo || !r)
|
|
686
|
+
return;
|
|
687
|
+
setElk(r);
|
|
688
|
+
if (dadosCabidos.current !== nodes) {
|
|
689
|
+
dadosCabidos.current = nodes;
|
|
690
|
+
setPedidoDeCaber(x => x + 1);
|
|
691
|
+
}
|
|
692
|
+
}, ESPERA_DO_ARRUMADOR);
|
|
693
|
+
return () => { vivo = false; clearTimeout(espera); };
|
|
694
|
+
// `entradaElk` muda junto com a chave; a chave é o que diz se mudou DE VERDADE.
|
|
695
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
696
|
+
}, [chaveElk]);
|
|
697
|
+
const autoDe = React.useCallback((id) => elk?.posicoes.get(id) ?? automatico.posicoes.get(id) ?? { x: 0, y: 0 }, [elk, automatico]);
|
|
698
|
+
// ── ONDE CADA UM ESTÁ (coordenada ABSOLUTA) ──────────────────────────────────────────────────
|
|
699
|
+
const posFinal = React.useMemo(() => {
|
|
700
|
+
const p = new Map();
|
|
701
|
+
for (const n of folhas) {
|
|
702
|
+
const auto = autoDe(n.id), mao = posMao.get(n.id);
|
|
703
|
+
// O `x`/`y` do contêiner não vale: a caixa dele é a dos filhos. Fechado, ele está onde a arrumação o pôs.
|
|
704
|
+
const proprio = contemFilhos.has(n.id) ? undefined : n;
|
|
705
|
+
p.set(n.id, mao ?? { x: proprio?.x ?? auto.x, y: proprio?.y ?? auto.y });
|
|
706
|
+
}
|
|
707
|
+
return p;
|
|
708
|
+
}, [folhas, posMao, autoDe, contemFilhos]);
|
|
709
|
+
const caixas = React.useMemo(() => {
|
|
710
|
+
const c = new Map();
|
|
711
|
+
for (const n of folhas) {
|
|
712
|
+
const p = posFinal.get(n.id), t = tamanho(n.id);
|
|
713
|
+
c.set(n.id, { x: p.x, y: p.y, larg: t.larg, alt: t.alt });
|
|
714
|
+
}
|
|
715
|
+
// O contêiner ABERTO envolve os filhos, mais o recheio e a cabeça — do mais fundo para o de fora.
|
|
716
|
+
for (const g of [...abertos].sort((a, b) => profundidade(b) - profundidade(a))) {
|
|
717
|
+
const filhos = [...c].filter(([id]) => pais.get(id) === g).map(([, x]) => x);
|
|
718
|
+
if (!filhos.length)
|
|
719
|
+
continue;
|
|
720
|
+
const x0 = Math.min(...filhos.map(f => f.x)) - RECHEIO_DO_GRUPO, y0 = Math.min(...filhos.map(f => f.y)) - RECHEIO_DO_GRUPO - CABECA_DO_GRUPO;
|
|
721
|
+
const x1 = Math.max(...filhos.map(f => f.x + f.larg)) + RECHEIO_DO_GRUPO, y1 = Math.max(...filhos.map(f => f.y + f.alt)) + RECHEIO_DO_GRUPO;
|
|
722
|
+
c.set(g, { x: x0, y: y0, larg: x1 - x0, alt: y1 - y0 });
|
|
723
|
+
}
|
|
724
|
+
return c;
|
|
725
|
+
}, [folhas, posFinal, tamanho, abertos, profundidade, pais]);
|
|
392
726
|
// ── O que fica aceso: o caminho pedido, ou o nó escolhido e os vizinhos ────────────────────
|
|
393
727
|
const ocultos = React.useMemo(() => new Set(hiddenIds ?? []), [hiddenIds]);
|
|
394
728
|
const aceso = React.useMemo(() => {
|
|
@@ -396,70 +730,72 @@ export function DependencyGraph({ nodes, edges, label, selectedId, onSelect, con
|
|
|
396
730
|
return { nos: EM_PAR(highlight.nodes), arestas: EM_PAR(highlight.edges) };
|
|
397
731
|
if (highlightNeighbors && selectedId) {
|
|
398
732
|
const nos = new Set([selectedId]), arestas = new Set();
|
|
399
|
-
const
|
|
733
|
+
const ident0 = arestasParalelas(edges, vertical ? m.larg : m.alt);
|
|
400
734
|
edges.forEach((e, i) => { if (e.from === selectedId || e.to === selectedId) {
|
|
401
735
|
nos.add(e.from);
|
|
402
736
|
nos.add(e.to);
|
|
403
|
-
arestas.add(
|
|
737
|
+
arestas.add(ident0[i].id);
|
|
404
738
|
} });
|
|
405
739
|
return { nos, arestas };
|
|
406
740
|
}
|
|
407
741
|
return null;
|
|
408
742
|
}, [highlight, highlightNeighbors, selectedId, edges, vertical, m]);
|
|
409
743
|
// As setas e o "ir até" moram no lado de DENTRO do provedor (eles precisam do motor); o nó chama
|
|
410
|
-
// por esta referência estável, sem refazer a lista de nós a cada render.
|
|
744
|
+
// por esta referência estável, sem refazer a lista de nós a cada render. O botão de fechar também.
|
|
411
745
|
const teclado = React.useRef(undefined);
|
|
412
|
-
const
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
//
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
}, [
|
|
746
|
+
const alternar = React.useRef(() => { });
|
|
747
|
+
const nosDoMotor = React.useMemo(() => {
|
|
748
|
+
const lista = [];
|
|
749
|
+
const relativa = (id) => { const c = caixas.get(id), p = paiAberto(id), cp = p ? caixas.get(p) : undefined; return cp ? { x: c.x - cp.x, y: c.y - cp.y } : { x: c.x, y: c.y }; };
|
|
750
|
+
const recolherDe = (id) => ({ fechado: fechados.has(id), n: guardados.get(id) ?? 0, alternar: () => alternar.current(id),
|
|
751
|
+
abrir: s.graphExpand, fechar: s.graphCollapse, ocultos: s.graphHidden });
|
|
752
|
+
const fixoDe = (id) => editavel && fixos.has(id) ? { nome: s.graphPinned } : undefined;
|
|
753
|
+
// O pai vem ANTES dos filhos na lista: é o que o motor pede para o filho andar junto com ele.
|
|
754
|
+
for (const g of [...abertos].filter(x => caixas.has(x)).sort((a, b) => profundidade(a) - profundidade(b))) {
|
|
755
|
+
const n = porId.get(g), c = caixas.get(g), pai = paiAberto(g);
|
|
756
|
+
lista.push({ id: g, type: "aureaGrupo", position: relativa(g), ...(pai ? { parentId: pai } : {}), width: c.larg, height: c.alt,
|
|
757
|
+
draggable: editavel && !fixos.has(g), selectable: editavel, focusable: editavel, hidden: ocultos.has(g),
|
|
758
|
+
ariaLabel: editavel ? [n.label, n.kind].filter(Boolean).join(", ") : undefined,
|
|
759
|
+
data: { label: n.label, kind: n.kind, icone: n.icon, alcas: alcasDoNo({ ...n, ports: undefined }, orientation, { larg: c.larg, alt: c.alt }),
|
|
760
|
+
recolher: recolherDe(g), apagado: aceso?.nos ? !aceso.nos.has(g) : false, fixo: fixoDe(g) } });
|
|
761
|
+
}
|
|
762
|
+
for (const n of folhas) {
|
|
763
|
+
const pai = paiAberto(n.id);
|
|
764
|
+
// O botão de fechar: todo contêiner (fechado, ele é folha) e, com `collapsible`, todo nó com subárvore.
|
|
765
|
+
const recolhivel = contemFilhos.has(n.id) || (!!collapsible && dominados.has(n.id));
|
|
766
|
+
lista.push({ id: n.id, type: "aurea", position: relativa(n.id), ...(pai ? { parentId: pai } : {}),
|
|
767
|
+
draggable: (editavel || !!onNodeMove) && !fixos.has(n.id),
|
|
768
|
+
// `selectable:false` no PRÓPRIO nó, como na aresta: no servidor (a prévia do catálogo) o motor ainda
|
|
769
|
+
// não recebeu o `elementsSelectable={false}` do fluxo, e o nó saía com a classe `.selectable` e a
|
|
770
|
+
// mão de ponteiro — num mapa sem `onSelect`, uma promessa de clique sem teclado. O gate
|
|
771
|
+
// `alvo-clicavel` pegou na página do padrão de rede (10/10/2026).
|
|
772
|
+
// (Rodada 2) No modo de arrumar, escolher é o trabalho: aí ele é escolhível e recebe o foco.
|
|
773
|
+
selectable: editavel, focusable: editavel,
|
|
774
|
+
ariaLabel: editavel ? [n.label, n.kind].filter(Boolean).join(", ") : undefined,
|
|
775
|
+
hidden: ocultos.has(n.id),
|
|
776
|
+
// ── O QUE FAZ ESTE GRAFO EXISTIR FORA DO NAVEGADOR ─────────────────────────────────
|
|
777
|
+
// O motor só desenha nó que tem tamanho, e no servidor não há o que medir. `initialWidth`
|
|
778
|
+
// e `initialHeight` valem SÓ até a primeira medição — no navegador o tamanho real assume
|
|
779
|
+
// e um nó de três linhas deixa de ser aproximado. E sem `handles` declarados a aresta não
|
|
780
|
+
// sabe de onde sai nem onde chega, então ela some.
|
|
781
|
+
//
|
|
782
|
+
// Eu tinha concluído que grafo não cabia em HTML estático e escrevi isso como limite do
|
|
783
|
+
// componente. Era conclusão minha, não medição: o motor tem caminho de SSR documentado, e
|
|
784
|
+
// são estas três coisas. A prévia do catálogo existe por causa deste bloco.
|
|
785
|
+
initialWidth: m.larg, initialHeight: m.alt,
|
|
786
|
+
handles: alcasDoNo(n, orientation, m).map(({ rotulo: _r, frac: _f, ...a }) => a),
|
|
787
|
+
data: { label: n.label, kind: n.kind, detail: n.detail, icone: n.icon, contagem: n.count, nomeDaContagem: n.countLabel, estado: n.status,
|
|
788
|
+
alcas: alcasDoNo(n, orientation, m), selecionado: selectedId === n.id, focado: focusId === n.id,
|
|
789
|
+
apagado: aceso?.nos ? !aceso.nos.has(n.id) : false,
|
|
790
|
+
conectavel: !!connectable, aoSelecionar: onSelect ? () => onSelect(n.id) : undefined,
|
|
791
|
+
aoTeclar: onSelect && !editavel ? (tecla) => teclado.current?.(n.id, tecla) : undefined,
|
|
792
|
+
nuvem: n.shape === "cloud", fixo: fixoDe(n.id), recolher: recolhivel ? recolherDe(n.id) : undefined, editavel },
|
|
793
|
+
});
|
|
794
|
+
}
|
|
795
|
+
return lista;
|
|
796
|
+
}, [abertos, caixas, paiAberto, profundidade, porId, editavel, fixos, ocultos, orientation, aceso, folhas, contemFilhos, collapsible, dominados,
|
|
797
|
+
onNodeMove, m, selectedId, focusId, connectable, onSelect, fechados, guardados, s]);
|
|
798
|
+
// ── A LISTA VIVA DO MOTOR ────────────────────────────────────────────────────────────────────
|
|
463
799
|
// ── A MEDIDA TEM DE PODER VOLTAR, e isto foi achado NO NAVEGADOR ───────────────────────
|
|
464
800
|
// A primeira versão passava `nodes` e nenhum `onNodesChange`. O motor aceita calado e o
|
|
465
801
|
// resultado é um grafo INVISÍVEL: ele só tira o `visibility:hidden` do nó depois de gravar
|
|
@@ -469,71 +805,579 @@ export function DependencyGraph({ nodes, edges, label, selectedId, onSelect, con
|
|
|
469
805
|
// não há layout para medir.
|
|
470
806
|
//
|
|
471
807
|
// A lista interna preserva `measured` quando os dados de fora mudam: sem isso, trocar a
|
|
472
|
-
// seleção jogaria a medida fora e o grafo piscaria a cada clique.
|
|
808
|
+
// seleção jogaria a medida fora e o grafo piscaria a cada clique. (Rodada 2) Preserva também a
|
|
809
|
+
// ESCOLHA do modo de arrumar — e a medida só passa entre nós do MESMO tipo: o contêiner que fecha
|
|
810
|
+
// vira nó comum, e a caixa grande de antes não é a medida dele.
|
|
473
811
|
const [nosVivos, setNosVivos] = React.useState(nosDoMotor);
|
|
474
812
|
React.useEffect(() => {
|
|
475
813
|
setNosVivos(anteriores => {
|
|
476
|
-
const
|
|
477
|
-
return nosDoMotor.map(n =>
|
|
814
|
+
const antes = new Map(anteriores.map(n => [n.id, n]));
|
|
815
|
+
return nosDoMotor.map(n => {
|
|
816
|
+
const a = antes.get(n.id), mesmo = !!a && a.type === n.type;
|
|
817
|
+
return semAlcasDeclaradas({ ...n, measured: mesmo ? a.measured : undefined, selected: editavel && mesmo ? a.selected : undefined });
|
|
818
|
+
});
|
|
478
819
|
});
|
|
479
|
-
}, [nosDoMotor]);
|
|
820
|
+
}, [nosDoMotor, editavel]);
|
|
821
|
+
const confirmar = React.useRef(() => { });
|
|
822
|
+
const abertosAgora = React.useRef(abertos);
|
|
823
|
+
abertosAgora.current = abertos;
|
|
480
824
|
const aoMudarNos = React.useCallback((mudancas) => {
|
|
481
825
|
setNosVivos(anteriores => applyNodeChanges(mudancas, anteriores).map(semAlcasDeclaradas));
|
|
826
|
+
const dims = mudancas.filter(c => c.type === "dimensions" && !!c.dimensions && !abertosAgora.current.has(c.id));
|
|
827
|
+
if (dims.length)
|
|
828
|
+
setMedidas(md => {
|
|
829
|
+
let b = null;
|
|
830
|
+
for (const c of dims)
|
|
831
|
+
if (c.type === "dimensions" && c.dimensions) {
|
|
832
|
+
const v = md.get(c.id), { width: larg, height: alt } = c.dimensions;
|
|
833
|
+
if (!v || Math.abs(v.larg - larg) > 0.5 || Math.abs(v.alt - alt) > 0.5) {
|
|
834
|
+
b ??= new Map(md);
|
|
835
|
+
b.set(c.id, { larg, alt });
|
|
836
|
+
}
|
|
837
|
+
}
|
|
838
|
+
return b ?? md;
|
|
839
|
+
});
|
|
840
|
+
// SOLTOU (fim do arraste) ou ANDOU PELA SETA: a mudança de posição que não está mais arrastando.
|
|
841
|
+
const soltos = mudancas.filter((c) => c.type === "position" && !c.dragging && !!c.position);
|
|
842
|
+
if (soltos.length)
|
|
843
|
+
confirmar.current(soltos);
|
|
482
844
|
}, []);
|
|
483
|
-
//
|
|
845
|
+
// ── O DESFAZER (MNT-13.5) E O AVISO PARA O APP GUARDAR (MNT-13.6) ─────────────────────────────
|
|
846
|
+
const pilha = React.useRef({ lista: [], ponteiro: 0 });
|
|
847
|
+
const [, setVersaoDaPilha] = React.useState(0);
|
|
848
|
+
const arrumacao = (pos, fix) => {
|
|
849
|
+
const positions = {};
|
|
850
|
+
for (const n of nodes) {
|
|
851
|
+
if (abertosLogicos.has(n.id))
|
|
852
|
+
continue;
|
|
853
|
+
const p = pos?.get(n.id) ?? posMao.get(n.id) ?? posFinal.get(n.id) ?? (n.x != null && n.y != null ? { x: n.x, y: n.y } : undefined);
|
|
854
|
+
if (p)
|
|
855
|
+
positions[n.id] = { x: Math.round(p.x), y: Math.round(p.y) };
|
|
856
|
+
}
|
|
857
|
+
return { positions, pinned: nodes.filter(n => fix?.get(n.id) ?? fixoMao.get(n.id) ?? n.pinned).map(n => n.id) };
|
|
858
|
+
};
|
|
859
|
+
const aplicar = (pos, fix) => {
|
|
860
|
+
if (pos.size)
|
|
861
|
+
setPosMao(x => { const b = new Map(x); for (const [k, v] of pos)
|
|
862
|
+
b.set(k, v); return b; });
|
|
863
|
+
if (fix?.size)
|
|
864
|
+
setFixoMao(x => { const b = new Map(x); for (const [k, v] of fix)
|
|
865
|
+
b.set(k, v); return b; });
|
|
866
|
+
onLayoutChange?.(arrumacao(pos, fix));
|
|
867
|
+
};
|
|
868
|
+
const executar = (c) => {
|
|
869
|
+
if (!c.depois.size && !c.fixosDepois?.size)
|
|
870
|
+
return;
|
|
871
|
+
const p = pilha.current;
|
|
872
|
+
p.lista = [...p.lista.slice(0, p.ponteiro), c].slice(-LIMITE_DO_DESFAZER);
|
|
873
|
+
p.ponteiro = p.lista.length;
|
|
874
|
+
setVersaoDaPilha(v => v + 1);
|
|
875
|
+
aplicar(c.depois, c.fixosDepois);
|
|
876
|
+
};
|
|
877
|
+
const posAntes = (ids) => new Map([...ids].map(id => [id, posFinal.get(id) ?? caixas.get(id) ?? { x: 0, y: 0 }]));
|
|
878
|
+
confirmar.current = mudancas => {
|
|
879
|
+
const depois = new Map();
|
|
880
|
+
// Primeiro os nós soltos; DEPOIS o contêiner, que arrasta os filhos junto pelo mesmo tanto.
|
|
881
|
+
const ordenadas = [...mudancas].sort((a, b) => Number(abertos.has(a.id)) - Number(abertos.has(b.id)));
|
|
882
|
+
for (const c of ordenadas) {
|
|
883
|
+
const pai = paiAberto(c.id), cp = pai ? caixas.get(pai) : undefined;
|
|
884
|
+
const abs = { x: c.position.x + (cp?.x ?? 0), y: c.position.y + (cp?.y ?? 0) };
|
|
885
|
+
if (abertos.has(c.id)) {
|
|
886
|
+
const antes = caixas.get(c.id);
|
|
887
|
+
if (!antes)
|
|
888
|
+
continue;
|
|
889
|
+
const dx = abs.x - antes.x, dy = abs.y - antes.y;
|
|
890
|
+
if (!dx && !dy)
|
|
891
|
+
continue;
|
|
892
|
+
for (const f of descendentes(c.id)) {
|
|
893
|
+
const p = depois.get(f) ?? posFinal.get(f);
|
|
894
|
+
if (p)
|
|
895
|
+
depois.set(f, { x: p.x + dx, y: p.y + dy });
|
|
896
|
+
}
|
|
897
|
+
}
|
|
898
|
+
else {
|
|
899
|
+
const antes = posFinal.get(c.id);
|
|
900
|
+
if (antes && Math.abs(antes.x - abs.x) < 0.5 && Math.abs(antes.y - abs.y) < 0.5)
|
|
901
|
+
continue;
|
|
902
|
+
depois.set(c.id, abs);
|
|
903
|
+
}
|
|
904
|
+
}
|
|
905
|
+
if (!depois.size)
|
|
906
|
+
return;
|
|
907
|
+
executar({ antes: posAntes(depois.keys()), depois });
|
|
908
|
+
if (onNodeMove)
|
|
909
|
+
for (const [id, p] of depois)
|
|
910
|
+
onNodeMove(id, p);
|
|
911
|
+
};
|
|
912
|
+
const escolhidos = () => nosVivos.filter(n => n.selected && !n.hidden).map(n => n.id);
|
|
913
|
+
const alinhaveis = () => escolhidos().filter(id => caixas.has(id) && !abertos.has(id));
|
|
914
|
+
const semFixos = (r) => { for (const id of [...r.keys()])
|
|
915
|
+
if (fixos.has(id))
|
|
916
|
+
r.delete(id); return r; };
|
|
917
|
+
const acoes = React.useRef(null);
|
|
918
|
+
acoes.current = {
|
|
919
|
+
desfazer() { const p = pilha.current; if (!p.ponteiro)
|
|
920
|
+
return; const c = p.lista[--p.ponteiro]; setVersaoDaPilha(v => v + 1); aplicar(c.antes, c.fixosAntes); },
|
|
921
|
+
refazer() { const p = pilha.current; if (p.ponteiro >= p.lista.length)
|
|
922
|
+
return; const c = p.lista[p.ponteiro++]; setVersaoDaPilha(v => v + 1); aplicar(c.depois, c.fixosDepois); },
|
|
923
|
+
alinhar(b) { const ids = alinhaveis(); if (ids.length < 2)
|
|
924
|
+
return; const r = semFixos(alinhar(new Map(ids.map(id => [id, caixas.get(id)])), b)); executar({ antes: posAntes(r.keys()), depois: r }); },
|
|
925
|
+
distribuir(e) { const ids = alinhaveis(); if (ids.length < 3)
|
|
926
|
+
return; const r = semFixos(distribuir(new Map(ids.map(id => [id, caixas.get(id)])), e)); executar({ antes: posAntes(r.keys()), depois: r }); },
|
|
927
|
+
fixar() {
|
|
928
|
+
const ids = escolhidos();
|
|
929
|
+
if (!ids.length)
|
|
930
|
+
return;
|
|
931
|
+
const todos = ids.every(id => fixos.has(id));
|
|
932
|
+
executar({ antes: new Map(), depois: new Map(), fixosAntes: new Map(ids.map(id => [id, fixos.has(id)])), fixosDepois: new Map(ids.map(id => [id, !todos])) });
|
|
933
|
+
},
|
|
934
|
+
// REORGANIZAR (MNT-13.4): todos voltam à arrumação automática, MENOS os fixos. O arrumador em camadas
|
|
935
|
+
// não tem alfinete por nó (pesquisado), então quem cairia em cima de um fixo é empurrado para o lado,
|
|
936
|
+
// na direção das camadas — e a linha que passar por ele desvia.
|
|
937
|
+
reorganizar() {
|
|
938
|
+
const presos = folhas.filter(n => fixos.has(n.id)).map(n => caixas.get(n.id));
|
|
939
|
+
const encosta = (a, b) => a.x < b.x + b.larg + VAO_Y && b.x < a.x + a.larg + VAO_Y && a.y < b.y + b.alt + VAO_Y && b.y < a.y + a.alt + VAO_Y;
|
|
940
|
+
const depois = new Map();
|
|
941
|
+
for (const n of folhas) {
|
|
942
|
+
if (fixos.has(n.id))
|
|
943
|
+
continue;
|
|
944
|
+
const t = tamanho(n.id), p = { ...autoDe(n.id) };
|
|
945
|
+
for (let k = 0; k <= presos.length; k++) {
|
|
946
|
+
const c = presos.find(c => encosta({ ...p, ...t }, c));
|
|
947
|
+
if (!c)
|
|
948
|
+
break;
|
|
949
|
+
if (vertical)
|
|
950
|
+
p.x = c.x + c.larg + VAO_Y;
|
|
951
|
+
else
|
|
952
|
+
p.y = c.y + c.alt + VAO_Y;
|
|
953
|
+
}
|
|
954
|
+
const antes = posFinal.get(n.id);
|
|
955
|
+
if (!antes || Math.abs(antes.x - p.x) >= 0.5 || Math.abs(antes.y - p.y) >= 0.5)
|
|
956
|
+
depois.set(n.id, p);
|
|
957
|
+
}
|
|
958
|
+
executar({ antes: posAntes(depois.keys()), depois });
|
|
959
|
+
setPedidoDeCaber(x => x + 1);
|
|
960
|
+
},
|
|
961
|
+
escolherTodos() { setNosVivos(ns => ns.map(n => n.selectable && !n.hidden ? { ...n, selected: true } : n)); },
|
|
962
|
+
arrumacao: () => arrumacao(),
|
|
963
|
+
};
|
|
964
|
+
alternar.current = id => { const novo = !fechados.has(id); setFechadoMao(x => new Map(x).set(id, novo)); onCollapseChange?.(id, novo); };
|
|
965
|
+
const escolhaMudou = React.useRef(() => { });
|
|
966
|
+
escolhaMudou.current = ids => { onSelectionChange?.(ids); if (ids.length === 1)
|
|
967
|
+
onSelect?.(ids[0]); };
|
|
968
|
+
const aoMudarEscolha = React.useCallback(({ nodes: sel }) => escolhaMudou.current(sel.map(n => n.id)), []);
|
|
969
|
+
const barra = { podeDesfazer: pilha.current.ponteiro > 0, podeRefazer: pilha.current.ponteiro < pilha.current.lista.length,
|
|
970
|
+
alinhaveis: alinhaveis().length, escolhidos: escolhidos().length, todosFixos: escolhidos().length > 0 && escolhidos().every(id => fixos.has(id)) };
|
|
971
|
+
// ── AS ROTAS (MNT-12.1 a 12.3) ─────────────────────────────────────────────────────────────
|
|
972
|
+
// Cada linha em degrau tem, nesta ordem: a rota do ARRUMADOR, se os dois nós continuam onde ele os pôs
|
|
973
|
+
// e nenhum nó entrou no caminho; o DESVIO já calculado, se as pontas não mudaram e o caminho segue
|
|
974
|
+
// livre; o DEGRAU SIMPLES, se ele não atravessa nó nenhum; e, se nada disso serve, ela vai para a fila
|
|
975
|
+
// do desvio — até a resposta, fica o degrau do motor. Na arrumação radial a linha é RETA, de borda a
|
|
976
|
+
// borda, na direção do outro centro. Durante o arraste, a linha de quem se move segue o motor ao vivo.
|
|
977
|
+
const chaveMovendo = React.useMemo(() => nosVivos.filter(n => n.dragging).map(n => n.id).sort().join("|"), [nosVivos]);
|
|
978
|
+
const movendo = React.useMemo(() => {
|
|
979
|
+
const base = chaveMovendo ? chaveMovendo.split("|") : [], out = new Set(base);
|
|
980
|
+
for (const g of base)
|
|
981
|
+
if (abertos.has(g))
|
|
982
|
+
for (const f of descendentes(g))
|
|
983
|
+
out.add(f);
|
|
984
|
+
return out;
|
|
985
|
+
}, [chaveMovendo, abertos, descendentes]);
|
|
986
|
+
const [desvios, setDesvios] = React.useState(() => new Map());
|
|
987
|
+
const tracados = React.useMemo(() => {
|
|
988
|
+
const mapa = new Map();
|
|
989
|
+
const pendentes = [];
|
|
990
|
+
const obstaculos = [...caixas].filter(([id]) => !abertos.has(id) && !ocultos.has(id));
|
|
991
|
+
const menos = (a, b) => obstaculos.filter(([id]) => id !== a && id !== b).map(([, c]) => c);
|
|
992
|
+
const todos = obstaculos.map(([, c]) => c);
|
|
993
|
+
const alcaDe = (id, porta, tipo) => {
|
|
994
|
+
const n = porId.get(id), c = caixas.get(id);
|
|
995
|
+
const lista = alcasDoNo(abertos.has(id) || contemFilhos.has(id) ? { ...n, ports: undefined } : n, orientation, { larg: c.larg, alt: c.alt });
|
|
996
|
+
return (porta && lista.find(a => a.id === porta && a.type === tipo)) || lista.find(a => !a.id && a.type === tipo);
|
|
997
|
+
};
|
|
998
|
+
desenhadas.forEach(({ item: e }, i) => {
|
|
999
|
+
const id = ident[i].id;
|
|
1000
|
+
if (ocultos.has(e.from) || ocultos.has(e.to) || movendo.has(e.from) || movendo.has(e.to))
|
|
1001
|
+
return;
|
|
1002
|
+
const cs = caixas.get(e.from), ct = caixas.get(e.to);
|
|
1003
|
+
if (!cs || !ct || !porId.has(e.from) || !porId.has(e.to))
|
|
1004
|
+
return;
|
|
1005
|
+
if (layout === "radial") {
|
|
1006
|
+
mapa.set(id, [bordaNaDirecao(cs, centroDe(ct)), bordaNaDirecao(ct, centroDe(cs))]);
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (edgeShape !== "step")
|
|
1010
|
+
return;
|
|
1011
|
+
const doElk = elk?.rotas.get(id);
|
|
1012
|
+
const noLugar = (no) => { const a = elk?.posicoes.get(no), c = caixas.get(no); return !!a && !!c && Math.abs(a.x - c.x) < 0.5 && Math.abs(a.y - c.y) < 0.5; };
|
|
1013
|
+
if (doElk && noLugar(e.from) && noLugar(e.to) && naBorda(doElk[0], cs) && naBorda(doElk[doElk.length - 1], ct) && !rotaCruza(doElk, menos(e.from, e.to))) {
|
|
1014
|
+
mapa.set(id, doElk);
|
|
1015
|
+
return;
|
|
1016
|
+
}
|
|
1017
|
+
const as = alcaDe(e.from, e.fromPort, "source"), at = alcaDe(e.to, e.toPort, "target");
|
|
1018
|
+
const desl = ident[i].deslocamento, ddx = vertical ? desl : 0, ddy = vertical ? 0 : desl;
|
|
1019
|
+
const ps = pontoDaAlca(cs, as), pt = pontoDaAlca(ct, at);
|
|
1020
|
+
const sp = { x: ps.x + ddx, y: ps.y + ddy }, tp = { x: pt.x + ddx, y: pt.y + ddy };
|
|
1021
|
+
const chave = `${sp.x},${sp.y},${tp.x},${tp.y}`;
|
|
1022
|
+
const salvo = desvios.get(id);
|
|
1023
|
+
// O desvio vale enquanto as pontas não mudam e nenhum nó ENTROU no caminho. O que já nasceu cruzando —
|
|
1024
|
+
// a ponta largada em cima de outro nó, e não há caminho livre — vale até as pontas mudarem: sem
|
|
1025
|
+
// isto, a linha voltava ao degrau do motor e pedia o mesmo desvio impossível de novo.
|
|
1026
|
+
if (salvo && salvo.chave === chave && (salvo.cruzava || !rotaCruza(salvo.pontos, menos(e.from, e.to)))) {
|
|
1027
|
+
mapa.set(id, salvo.pontos);
|
|
1028
|
+
return;
|
|
1029
|
+
}
|
|
1030
|
+
const degrau = esqueleto(sp, as.position, tp, at.position);
|
|
1031
|
+
if (degrau && !rotaCruza(degrau, todos)) {
|
|
1032
|
+
mapa.set(id, degrau);
|
|
1033
|
+
return;
|
|
1034
|
+
}
|
|
1035
|
+
pendentes.push({ id, de: e.from, para: e.to, s: sp, t: tp, ls: as.position, lt: at.position, chave });
|
|
1036
|
+
});
|
|
1037
|
+
return { mapa, pendentes, obstaculos };
|
|
1038
|
+
}, [desenhadas, ident, caixas, abertos, ocultos, movendo, porId, contemFilhos, orientation, layout, edgeShape, elk, desvios, vertical]);
|
|
1039
|
+
const chavePendentes = tracados.pendentes.map(p => `${p.id}@${p.chave}`).join("|");
|
|
1040
|
+
React.useEffect(() => {
|
|
1041
|
+
if (!tracados.pendentes.length || movendo.size)
|
|
1042
|
+
return;
|
|
1043
|
+
let vivo = true;
|
|
1044
|
+
(async () => {
|
|
1045
|
+
const se = await desviador();
|
|
1046
|
+
if (!se || !vivo)
|
|
1047
|
+
return;
|
|
1048
|
+
const nosObstaculo = tracados.obstaculos.map(([id, c]) => ({ id, position: { x: c.x, y: c.y }, measured: { width: c.larg, height: c.alt }, data: {} }));
|
|
1049
|
+
const novos = new Map(), lote = tracados.pendentes;
|
|
1050
|
+
const caixasDe = (a, b) => tracados.obstaculos.filter(([id]) => id !== a && id !== b).map(([, c]) => c);
|
|
1051
|
+
for (let i = 0; i < lote.length; i += LOTE_DO_DESVIO) {
|
|
1052
|
+
const parte = lote.slice(i, i + LOTE_DO_DESVIO);
|
|
1053
|
+
const r = se.routeSmartEdgeBatch(nosObstaculo, parte.map(p => ({ id: p.id, source: p.de, target: p.para, sourceX: p.s.x, sourceY: p.s.y, targetX: p.t.x, targetY: p.t.y,
|
|
1054
|
+
sourcePosition: p.ls, targetPosition: p.lt, preset: "step", options: { nodePadding: FOLGA_DO_DESVIO, gridRatio: MALHA_DO_DESVIO } })));
|
|
1055
|
+
for (const p of parte) {
|
|
1056
|
+
const x = r[p.id];
|
|
1057
|
+
if (x?.kind !== "routed")
|
|
1058
|
+
continue;
|
|
1059
|
+
const pontos = simplificar([p.s, ...x.points.map(([a, b]) => ({ x: a, y: b })), p.t]);
|
|
1060
|
+
novos.set(p.id, { chave: p.chave, pontos, cruzava: rotaCruza(pontos, caixasDe(p.de, p.para)) });
|
|
1061
|
+
}
|
|
1062
|
+
if (i + LOTE_DO_DESVIO < lote.length) {
|
|
1063
|
+
await new Promise(ok => setTimeout(ok, 0));
|
|
1064
|
+
if (!vivo)
|
|
1065
|
+
return;
|
|
1066
|
+
}
|
|
1067
|
+
}
|
|
1068
|
+
if (vivo && novos.size)
|
|
1069
|
+
setDesvios(d => { const b = new Map(d); for (const [k, v] of novos)
|
|
1070
|
+
b.set(k, v); return b; });
|
|
1071
|
+
})();
|
|
1072
|
+
return () => { vivo = false; };
|
|
1073
|
+
// A fila muda junto com o traçado; a chave é o que diz se ela mudou DE VERDADE.
|
|
1074
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
1075
|
+
}, [chavePendentes, movendo.size]);
|
|
1076
|
+
// As PONTES (MNT-12.3): a linha de depois salta sobre as de antes, na ordem da lista do app.
|
|
1077
|
+
const saltos = React.useMemo(() => pontes(ident.filter(x => tracados.mapa.has(x.id)).map(x => ({ id: x.id, pontos: tracados.mapa.get(x.id) })), RAIO_DA_PONTE, CANTO_DO_DEGRAU), [ident, tracados]);
|
|
1078
|
+
// `selectable:false` na PRÓPRIA aresta, e não só no `elementsSelectable` do `<ReactFlow>`:
|
|
1079
|
+
// medido em 29/08/2026, a prop do fluxo não tira a classe `.selectable` da aresta, que é onde
|
|
1080
|
+
// o `base.css` do motor põe `cursor:pointer`. A mão de ponteiro prometia um clique que a API
|
|
1081
|
+
// da Aurea não atende — `onSelect` é do NÓ, e quem o atende é o <button> do corpo dele.
|
|
1082
|
+
// (10/10/2026) Com `onEdgeSelect` a aresta passa a ser alvo de verdade, e aí a mão é honesta.
|
|
1083
|
+
// (Rodada 2) E só aí ela recebe o Tab: medido na bancada da rodada 1, TODA linha recebia o Tab, com
|
|
1084
|
+
// o nome do motor em inglês ("Edge from internet to fw") e a instrução de apagar com Delete. Agora o
|
|
1085
|
+
// nome é nosso ("Ligação: Internet – Firewall") e o Enter escolhe a linha.
|
|
1086
|
+
const nomeDe = React.useCallback((id) => porId.get(id)?.label ?? id, [porId]);
|
|
1087
|
+
const arestasDoMotor = React.useMemo(() => desenhadas.map(({ item: e, grupo }, i) => {
|
|
1088
|
+
const id = ident[i].id, rota = tracados.mapa.get(id), sl = saltos.get(id);
|
|
1089
|
+
return { id, type: "aurea", source: e.from, target: e.to, sourceHandle: e.fromPort, targetHandle: e.toPort,
|
|
1090
|
+
// O grupo FECHADO é a linha de todos: o nome da primeira enganaria ("WAN 1" valendo pelas 10).
|
|
1091
|
+
label: grupo && !grupo.aberto ? undefined : e.label, animated: e.animated, selectable: !!onEdgeSelect, focusable: !!onEdgeSelect,
|
|
1092
|
+
ariaLabel: `${s.graphEdge}: ${nomeDe(e.from)} – ${nomeDe(e.to)}${e.label ? ` (${e.label})` : ""}`,
|
|
1093
|
+
...(onEdgeSelect ? { domAttributes: { onKeyDown: (ev) => { if (ev.key === "Enter" || ev.key === " ") {
|
|
1094
|
+
ev.preventDefault();
|
|
1095
|
+
onEdgeSelect(id);
|
|
1096
|
+
} } } } : {}),
|
|
1097
|
+
hidden: ocultos.has(e.from) || ocultos.has(e.to),
|
|
1098
|
+
data: { deslocamento: rota ? 0 : ident[i].deslocamento, vertical, forma: edgeShape, padrao: e.pattern, peso: e.weight, tom: e.status?.tone, marca: e.mark, marcaDoEstado: e.status?.mark, contagem: e.count,
|
|
1099
|
+
origem: e.sourceLabel, destino: e.targetLabel, apagada: aceso?.arestas ? !aceso.arestas.has(e.id ?? id) : aceso?.nos ? !(aceso.nos.has(e.from) && aceso.nos.has(e.to)) : false,
|
|
1100
|
+
grupo: grupo ? { n: grupo.n, aberto: grupo.aberto, alternar: () => alternarGrupo(grupo.chave), mostrar: s.graphShow, juntar: s.graphJoin, conexoes: s.graphConnections } : undefined,
|
|
1101
|
+
rota, saltos: sl ? [...sl] : undefined, prioridade: e.priority, ordem: i },
|
|
1102
|
+
};
|
|
1103
|
+
}), [desenhadas, ident, tracados, saltos, vertical, edgeShape, onEdgeSelect, ocultos, aceso, alternarGrupo, s, nomeDe]);
|
|
1104
|
+
// A LEGENDA se monta com as linhas, em DUAS PARTES (decisão do Victor, 12/10/2026, depois da pesquisa
|
|
1105
|
+
// nas referências de mapa dele): o TIPO da ligação (`legend`, pelo desenho, SEM cor) e o ESTADO
|
|
1106
|
+
// (`status`, pela cor). Nenhuma das referências de mapa junta as duas coisas numa lista: uma legenda só
|
|
1107
|
+
// o tipo, pelo desenho; as outras pintam o estado. Cada amostra mostra o que TODAS as linhas do grupo têm em comum
|
|
1108
|
+
// — a espessura da velocidade varia dentro da "Fibra" e não é dela. A marca de cada parte é a SUA: o
|
|
1109
|
+
// `mark` da linha na do tipo (o cadeado da VPN), o `status.mark` na do estado (o × do "Caído"). Com uma
|
|
1110
|
+
// linha só não dá para adivinhar de quem é a marca — por isso o app diz.
|
|
484
1111
|
const legenda = React.useMemo(() => {
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
1112
|
+
const comum = (xs) => xs.every(x => x === xs[0]) ? xs[0] : undefined;
|
|
1113
|
+
const agrupar = (nome) => {
|
|
1114
|
+
const m = new Map();
|
|
1115
|
+
for (const e of edges) {
|
|
1116
|
+
const k = nome(e);
|
|
1117
|
+
if (k)
|
|
1118
|
+
m.set(k, [...(m.get(k) ?? []), e]);
|
|
1119
|
+
}
|
|
1120
|
+
return [...m.entries()];
|
|
1121
|
+
};
|
|
1122
|
+
const estados = agrupar(e => e.status?.label).map(([nome, es]) => ({ nome, tom: comum(es.map(e => e.status?.tone)), marca: comum(es.map(e => e.status?.mark)) }));
|
|
1123
|
+
const tipos = agrupar(e => e.legend).map(([nome, es]) => ({ nome, padrao: comum(es.map(e => e.pattern)), peso: comum(es.map(e => e.weight)), marca: comum(es.map(e => e.mark)) }));
|
|
1124
|
+
return { tipos, estados };
|
|
490
1125
|
}, [edges]);
|
|
1126
|
+
const idDaLegenda = React.useId();
|
|
1127
|
+
// O texto que o MOTOR põe na tela (descrição do nó para o leitor de tela, aviso de "movido", nome das
|
|
1128
|
+
// alças e do minimapa) sai na língua da Aurea — o padrão dele é inglês.
|
|
1129
|
+
const rotulosDoMotor = React.useMemo(() => ({
|
|
1130
|
+
"node.a11yDescription.default": s.graphNodeHelp, "node.a11yDescription.keyboardDisabled": s.graphNodeHelp,
|
|
1131
|
+
"node.a11yDescription.ariaLiveMessage": ({ x, y }) => `${s.graphMoved}: x ${x}, y ${y}`,
|
|
1132
|
+
"edge.a11yDescription.default": s.graphEdgeHelp, "controls.ariaLabel": s.graphLabel, "controls.zoomIn.ariaLabel": s.graphZoomIn,
|
|
1133
|
+
"controls.zoomOut.ariaLabel": s.graphZoomOut, "controls.fitView.ariaLabel": s.graphFit, "controls.interactive.ariaLabel": s.graphArrange,
|
|
1134
|
+
"minimap.ariaLabel": s.graphOverview, "handle.ariaLabel": s.graphHandle
|
|
1135
|
+
}), [s]);
|
|
1136
|
+
const linhas = React.useMemo(() => desenhadas.map(({ item }, i) => ({ item, rota: tracados.mapa.get(ident[i].id) })), [desenhadas, tracados, ident]);
|
|
491
1137
|
const raiz = React.useRef(null);
|
|
1138
|
+
// MNT-21 (opção "A" do Victor, 12/10/2026): O MAPA DE LEITURA É UMA PARADA DE TAB. O app mediu 87
|
|
1139
|
+
// paradas, as 59 linhas antes de qualquer nó (o motor desenha as linhas antes dos nós). Agora só o nó
|
|
1140
|
+
// atual (o último visitado; no começo, o primeiro) fica na ordem do Tab; as setas andam de nó em nó;
|
|
1141
|
+
// com o nó em foco, o Tab passa pelo botão de fechar dele e pelas linhas dele, e depois sai do mapa.
|
|
1142
|
+
// Numa linha em foco, as setas levam às pontas dela. O link de crédito do motor sai do Tab (continua à
|
|
1143
|
+
// vista). No modo de arrumar quem manda é o motor, e nada disto liga.
|
|
1144
|
+
const linhasDoNo = React.useMemo(() => {
|
|
1145
|
+
const porNo = new Map(), pontas = new Map();
|
|
1146
|
+
desenhadas.forEach(({ item }, i) => {
|
|
1147
|
+
const id = ident[i].id;
|
|
1148
|
+
pontas.set(id, { de: item.from, para: item.to });
|
|
1149
|
+
for (const n of [item.from, item.to]) {
|
|
1150
|
+
const l = porNo.get(n) ?? [];
|
|
1151
|
+
if (!l.includes(id))
|
|
1152
|
+
l.push(id);
|
|
1153
|
+
porNo.set(n, l);
|
|
1154
|
+
}
|
|
1155
|
+
});
|
|
1156
|
+
const pai = new Map(nodes.filter(n => n.parentId).map(n => [n.id, n.parentId]));
|
|
1157
|
+
return { porNo, pontas, pai };
|
|
1158
|
+
}, [desenhadas, ident, nodes]);
|
|
1159
|
+
const linhasRef = React.useRef(linhasDoNo);
|
|
1160
|
+
linhasRef.current = linhasDoNo;
|
|
1161
|
+
React.useEffect(() => {
|
|
1162
|
+
const el = raiz.current;
|
|
1163
|
+
if (!el || editavel)
|
|
1164
|
+
return;
|
|
1165
|
+
let atual = null, quadro = 0;
|
|
1166
|
+
const sequencia = (id) => {
|
|
1167
|
+
const no = el.querySelector(`.react-flow__node[data-id="${escapar(id)}"]`);
|
|
1168
|
+
if (!no)
|
|
1169
|
+
return [];
|
|
1170
|
+
const ancora = ancoraDoNo(el, id);
|
|
1171
|
+
const seq = ancora ? [ancora, ...[...no.querySelectorAll("button.graph-node-toggle, button.graph-group-toggle")].filter(b => b !== ancora)] : [];
|
|
1172
|
+
// O nó dentro de um contêiner aberto leva também o "Fechar" dele: o contêiner aberto não é alvo das
|
|
1173
|
+
// setas (o centro dele é o meio da caixa grande), e o botão dele não pode ficar fora de alcance.
|
|
1174
|
+
const pai = linhasRef.current.pai.get(id);
|
|
1175
|
+
const fecharPai = pai ? el.querySelector(`.react-flow__node[data-id="${escapar(pai)}"] button.graph-group-toggle`) : null;
|
|
1176
|
+
if (fecharPai && !seq.includes(fecharPai))
|
|
1177
|
+
seq.push(fecharPai);
|
|
1178
|
+
for (const l of linhasRef.current.porNo.get(id) ?? []) {
|
|
1179
|
+
const linha = el.querySelector(`.react-flow__edge[data-id="${escapar(l)}"]`);
|
|
1180
|
+
if (linha && linha.hasAttribute("tabindex"))
|
|
1181
|
+
seq.push(linha);
|
|
1182
|
+
const grupo = el.querySelector(`[data-edge-group="${escapar(l)}"]`);
|
|
1183
|
+
if (grupo)
|
|
1184
|
+
seq.push(grupo);
|
|
1185
|
+
}
|
|
1186
|
+
return seq;
|
|
1187
|
+
};
|
|
1188
|
+
const aplicar = () => {
|
|
1189
|
+
const todos = [...el.querySelectorAll(".react-flow__node button, .react-flow__edge[tabindex], .graph-edge-group, .react-flow__attribution a")];
|
|
1190
|
+
if (!atual || !ancoraDoNo(el, atual)) {
|
|
1191
|
+
const ids = [...el.querySelectorAll(".react-flow__node")].map(n => n.dataset.id);
|
|
1192
|
+
atual = ids.find(id => el.querySelector(`.react-flow__node[data-id="${escapar(id)}"] button.graph-node-body`))
|
|
1193
|
+
?? ids.find(id => ancoraDoNo(el, id)) ?? null;
|
|
1194
|
+
}
|
|
1195
|
+
const ancora = atual ? ancoraDoNo(el, atual) : null;
|
|
1196
|
+
for (const t of todos)
|
|
1197
|
+
t.tabIndex = t === ancora ? 0 : -1;
|
|
1198
|
+
};
|
|
1199
|
+
const depois = () => { cancelAnimationFrame(quadro); quadro = requestAnimationFrame(aplicar); };
|
|
1200
|
+
const aoFocar = (e) => {
|
|
1201
|
+
const no = e.target.closest(".react-flow__node");
|
|
1202
|
+
if (no?.dataset.id && no.dataset.id !== atual) {
|
|
1203
|
+
atual = no.dataset.id;
|
|
1204
|
+
aplicar();
|
|
1205
|
+
}
|
|
1206
|
+
};
|
|
1207
|
+
const aoTeclar = (e) => {
|
|
1208
|
+
const alvo = e.target;
|
|
1209
|
+
if (!atual)
|
|
1210
|
+
return;
|
|
1211
|
+
const seq = sequencia(atual), i = seq.indexOf(alvo);
|
|
1212
|
+
if (i < 0)
|
|
1213
|
+
return;
|
|
1214
|
+
if (e.key === "Tab") {
|
|
1215
|
+
if (e.shiftKey ? i > 0 : i < seq.length - 1) {
|
|
1216
|
+
e.preventDefault();
|
|
1217
|
+
seq[e.shiftKey ? i - 1 : i + 1].focus({ preventScroll: true });
|
|
1218
|
+
return;
|
|
1219
|
+
}
|
|
1220
|
+
// A última da sequência: o Tab do navegador segue para depois do mapa. A âncora sai da ordem por
|
|
1221
|
+
// um instante — ela vem DEPOIS das linhas no documento, e o Tab voltaria a ela.
|
|
1222
|
+
if (!e.shiftKey) {
|
|
1223
|
+
const a = seq[0];
|
|
1224
|
+
a.tabIndex = -1;
|
|
1225
|
+
setTimeout(aplicar, 0);
|
|
1226
|
+
}
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
if (!SETAS.has(e.key))
|
|
1230
|
+
return;
|
|
1231
|
+
const linha = alvo.closest(".react-flow__edge")?.dataset.id ?? alvo.dataset.edgeGroup;
|
|
1232
|
+
if (linha) {
|
|
1233
|
+
// Numa linha, as setas levam às pontas: para cima ou para a esquerda, a de onde ela sai.
|
|
1234
|
+
const pt = linhasRef.current.pontas.get(linha);
|
|
1235
|
+
if (!pt)
|
|
1236
|
+
return;
|
|
1237
|
+
e.preventDefault();
|
|
1238
|
+
ancoraDoNo(el, e.key === "ArrowUp" || e.key === "ArrowLeft" ? pt.de : pt.para)?.focus({ preventScroll: true });
|
|
1239
|
+
return;
|
|
1240
|
+
}
|
|
1241
|
+
// O botão do nó já anda pelas setas; o de fechar anda igual.
|
|
1242
|
+
if (!alvo.matches("button.graph-node-body")) {
|
|
1243
|
+
e.preventDefault();
|
|
1244
|
+
teclado.current?.(atual, e.key);
|
|
1245
|
+
}
|
|
1246
|
+
};
|
|
1247
|
+
aplicar();
|
|
1248
|
+
const obs = new MutationObserver(depois);
|
|
1249
|
+
obs.observe(el, { childList: true, subtree: true });
|
|
1250
|
+
el.addEventListener("focusin", aoFocar);
|
|
1251
|
+
el.addEventListener("keydown", aoTeclar);
|
|
1252
|
+
return () => { obs.disconnect(); cancelAnimationFrame(quadro); el.removeEventListener("focusin", aoFocar); el.removeEventListener("keydown", aoTeclar); };
|
|
1253
|
+
}, [editavel]);
|
|
492
1254
|
// `initialWidth`/`initialHeight` no provider é o par do de cima, um nível acima: é o viewport
|
|
493
1255
|
// que o `fitView` usa ENQUANTO ninguém mediu. Com o prefixo `initial`, o navegador sobrescreve
|
|
494
1256
|
// na primeira medição — passar `width`/`height` direto no ReactFlow fixaria o tamanho e mataria
|
|
495
1257
|
// a fluidez. O valor é a extensão do próprio layout, então no servidor o grafo cabe exato.
|
|
496
1258
|
const estilo = { ...style, ...(height ? { blockSize: height } : null), "--graph-node-w": `${m.larg}px` };
|
|
497
|
-
return _jsx("div", { ref: raiz, className: cx("dependency-graph", className), style: estilo, "data-text": textSize === "md" ? undefined : textSize, "data-contrast": highContrast ? "high" : undefined, role: "group", "aria-label": label ?? s.graphLabel, ...props, children: _jsx(ReactFlowProvider, { initialNodes: nosDoMotor, initialEdges: arestasDoMotor, fitView: true, initialWidth: automatico.extensao.larg, initialHeight: automatico.extensao.alt, children: _jsxs(ReactFlow, { nodes: nosVivos, onNodesChange: aoMudarNos, edges: arestasDoMotor, nodeTypes: TIPOS, edgeTypes: TIPOS_DE_ARESTA, fitView: true, proOptions: { hideAttribution: false }, nodesDraggable: !!onNodeMove, nodesConnectable: !!connectable, elementsSelectable:
|
|
498
|
-
onConnect({ from: c.source, to: c.target, fromPort: c.sourceHandle ?? undefined, toPort: c.targetHandle ?? undefined }); } : undefined, children: [_jsx(Background, {}), _jsx(Comandos, { raiz: raiz, teclado: teclado, focusId: focusId,
|
|
1259
|
+
return _jsx("div", { ref: raiz, className: cx("dependency-graph", className), style: estilo, "data-text": textSize === "md" ? undefined : textSize, "data-contrast": highContrast ? "high" : undefined, "data-editable": editavel || undefined, role: "group", "aria-label": label ?? s.graphLabel, ...props, children: _jsx(ReactFlowProvider, { initialNodes: nosDoMotor, initialEdges: arestasDoMotor, fitView: true, initialWidth: automatico.extensao.larg, initialHeight: automatico.extensao.alt, children: _jsxs(ReactFlow, { nodes: nosVivos, onNodesChange: aoMudarNos, edges: arestasDoMotor, nodeTypes: TIPOS, edgeTypes: TIPOS_DE_ARESTA, fitView: true, proOptions: { hideAttribution: false }, minZoom: ESCALA_MINIMA, nodesDraggable: editavel || !!onNodeMove, nodesConnectable: !!connectable, elementsSelectable: editavel, nodesFocusable: editavel, edgesFocusable: !!onEdgeSelect, disableKeyboardA11y: !editavel, selectionOnDrag: editavel, panOnDrag: editavel ? BOTOES_DE_ARRASTAR : true, selectionMode: SelectionMode.Partial, multiSelectionKeyCode: TECLAS_DE_SOMAR, deleteKeyCode: null, snapToGrid: editavel && grid, snapGrid: [GRADE, GRADE], onlyRenderVisibleElements: !!visibleOnly, ariaLabelConfig: rotulosDoMotor, onSelectionChange: editavel ? aoMudarEscolha : undefined, onNodeMouseEnter: onNodeHover ? (_, no) => onNodeHover(no.id) : undefined, onNodeMouseLeave: onNodeHover ? () => onNodeHover(null) : undefined, onNodeContextMenu: onNodeContextMenu ? (ev, no) => { ev.preventDefault(); onNodeContextMenu(no.id, { x: ev.clientX, y: ev.clientY }); } : undefined, onEdgeClick: onEdgeSelect ? (_, a) => onEdgeSelect(a.id) : undefined, onConnect: onConnect ? c => { if (c.source && c.target)
|
|
1260
|
+
onConnect({ from: c.source, to: c.target, fromPort: c.sourceHandle ?? undefined, toPort: c.targetHandle ?? undefined }); } : undefined, children: [_jsx(Background, { gap: editavel && grid ? GRADE : undefined }), _jsx(Comandos, { raiz: raiz, teclado: teclado, focusId: focusId, pedidoDeCaber: pedidoDeCaber, controls: !!controls, apiRef: apiRef, linhas: linhas, forma: edgeShape, titulo: label ?? s.graphLabel, desenho: tracados, editavel: editavel, barra: barra, acoes: acoes }), minimap && _jsx(MiniMap, { pannable: true, zoomable: true, ariaLabel: s.graphOverview, className: "graph-minimap" }), legend && (legenda.tipos.length > 0 || legenda.estados.length > 0) && _jsx(Panel, { position: "bottom-left", children: _jsxs("div", { className: "graph-legend", role: "group", "aria-label": s.graphLegend, children: [legenda.tipos.length > 0 && _jsxs(_Fragment, { children: [legenda.estados.length > 0 && _jsx("p", { className: "graph-legend-title", id: `${idDaLegenda}-tipo`, children: s.graphLegendKind }), _jsx("ul", { "aria-labelledby": legenda.estados.length > 0 ? `${idDaLegenda}-tipo` : undefined, children: legenda.tipos.map(t => _jsxs("li", { children: [_jsx(AmostraDaLinha, { padrao: t.padrao, peso: t.peso, marca: t.marca }), _jsx("span", { children: t.nome })] }, t.nome)) })] }), legenda.estados.length > 0 && _jsxs(_Fragment, { children: [legenda.tipos.length > 0 && _jsx("p", { className: "graph-legend-title", id: `${idDaLegenda}-estado`, children: s.graphLegendStatus }), _jsx("ul", { "aria-labelledby": legenda.tipos.length > 0 ? `${idDaLegenda}-estado` : undefined, children: legenda.estados.map(e => _jsxs("li", { children: [_jsx(AmostraDaLinha, { tom: e.tom, marca: e.marca }), _jsx("span", { children: e.nome })] }, e.nome)) })] })] }) })] }) }) });
|
|
1261
|
+
}
|
|
1262
|
+
/** O primeiro alvo de Tab de um nó no mapa de leitura: o botão dele; sem ele, o de fechar. */
|
|
1263
|
+
function ancoraDoNo(raiz, id) {
|
|
1264
|
+
const no = raiz?.querySelector(`.react-flow__node[data-id="${escapar(id)}"]`);
|
|
1265
|
+
return no?.querySelector("button.graph-node-body") ?? no?.querySelector("button.graph-node-toggle, button.graph-group-toggle") ?? null;
|
|
499
1266
|
}
|
|
500
1267
|
// `CSS.escape` existe no navegador; fora dele (o teste em jsdom), as aspas bastam.
|
|
501
1268
|
const escapar = (id) => typeof CSS !== "undefined" && typeof CSS.escape === "function" ? CSS.escape(id) : id.replace(/[\x22\\]/g, "\\$&");
|
|
502
1269
|
/**
|
|
503
|
-
*
|
|
504
|
-
*
|
|
1270
|
+
* OS RÓTULOS QUE SE ENCOSTAM (MNT-12.5): mede cada rótulo na tela e esconde, pela prioridade, o que
|
|
1271
|
+
* cobre outro rótulo ou um nó. Medir na tela vale para qualquer escala — o rótulo e o nó crescem
|
|
1272
|
+
* juntos com o zoom. O atributo é posto à mão, fora do React, e tirado antes de cada nova conta.
|
|
1273
|
+
*/
|
|
1274
|
+
function esconderRotulos(el) {
|
|
1275
|
+
if (!el)
|
|
1276
|
+
return;
|
|
1277
|
+
const caixa = (e) => { const b = e.getBoundingClientRect(); return { x: b.left, y: b.top, larg: b.width, alt: b.height }; };
|
|
1278
|
+
const rotulos = [...el.querySelectorAll("[data-edge-label]")];
|
|
1279
|
+
for (const r of rotulos)
|
|
1280
|
+
r.removeAttribute("data-crowded");
|
|
1281
|
+
const medidos = rotulos.map(r => ({ id: r.getAttribute("data-edge-label"), caixa: caixa(r), prioridade: Number(r.getAttribute("data-priority")) || 0,
|
|
1282
|
+
ordem: Number(r.getAttribute("data-order")) || 0, el: r })).filter(r => r.caixa.larg > 0).sort((a, b) => a.ordem - b.ordem);
|
|
1283
|
+
const somem = rotulosQueSomem(medidos, [...el.querySelectorAll(".graph-node")].map(caixa));
|
|
1284
|
+
for (const r of medidos)
|
|
1285
|
+
if (somem.has(r.id))
|
|
1286
|
+
r.el.setAttribute("data-crowded", "");
|
|
1287
|
+
}
|
|
1288
|
+
/**
|
|
1289
|
+
* O lado de DENTRO do provedor: o que precisa do motor — caber na tela depois da arrumação, o "ir
|
|
1290
|
+
* até", as setas de nó em nó, os botões de zoom, a barra do modo de arrumar, os rótulos que somem e
|
|
1291
|
+
* o `apiRef`.
|
|
505
1292
|
*/
|
|
506
|
-
function Comandos({ raiz, teclado, focusId,
|
|
1293
|
+
function Comandos({ raiz, teclado, focusId, pedidoDeCaber, controls, apiRef, linhas, forma, titulo, desenho, editavel, barra, acoes }) {
|
|
507
1294
|
const rf = useReactFlow();
|
|
508
1295
|
const s = useAureaStrings();
|
|
509
1296
|
// Quem pediu menos movimento não vê a tela deslizar: o "ir até" e o caber na tela pulam direto.
|
|
510
1297
|
const duracao = () => typeof matchMedia === "function" && matchMedia("(prefers-reduced-motion: reduce)").matches ? 0 : 300;
|
|
1298
|
+
// O centro de um nó NO MAPA: o filho de um contêiner tem posição RELATIVA ao pai, e o motor guarda a absoluta.
|
|
1299
|
+
const centro = React.useCallback((id) => {
|
|
1300
|
+
const n = rf.getInternalNode(id);
|
|
1301
|
+
if (!n)
|
|
1302
|
+
return null;
|
|
1303
|
+
const w = n.measured?.width ?? n.width ?? n.initialWidth ?? LARGURA, h = n.measured?.height ?? n.height ?? n.initialHeight ?? ALTURA;
|
|
1304
|
+
return { x: n.internals.positionAbsolute.x + w / 2, y: n.internals.positionAbsolute.y + h / 2 };
|
|
1305
|
+
}, [rf]);
|
|
511
1306
|
const centralizar = React.useCallback((id) => {
|
|
512
|
-
const
|
|
513
|
-
if (
|
|
1307
|
+
const c = centro(id);
|
|
1308
|
+
if (c)
|
|
1309
|
+
rf.setCenter(c.x, c.y, { zoom: Math.max(rf.getZoom(), 1), duration: duracao() });
|
|
1310
|
+
}, [rf, centro]);
|
|
1311
|
+
// MNT-24 (12/10/2026): A BARRA DO MODO DE ARRUMAR COBRIA O TOPO DO MAPA — o nó de cima ficava atrás
|
|
1312
|
+
// dela até a pessoa rolar (medido pelo app). Os botões de zoom, no outro canto de cima, fazem o mesmo.
|
|
1313
|
+
// O "caber na tela" passa a reservar em cima a altura do mais alto dos dois, com a margem que o motor
|
|
1314
|
+
// dá a eles (`offsetTop`) dos dois lados; os outros lados ficam com a margem do motor (0,1, o padrão
|
|
1315
|
+
// dele). A reserva é MEDIDA: a barra quebra em mais linhas na tela estreita.
|
|
1316
|
+
const barraRef = React.useRef(null);
|
|
1317
|
+
const zoomRef = React.useRef(null);
|
|
1318
|
+
const [reserva, setReserva] = React.useState(0);
|
|
1319
|
+
React.useLayoutEffect(() => {
|
|
1320
|
+
const medir = () => {
|
|
1321
|
+
const alturas = [barraRef.current, zoomRef.current].filter((e) => !!e).map(e => e.offsetTop * 2 + e.offsetHeight);
|
|
1322
|
+
setReserva(alturas.length ? Math.max(...alturas) : 0);
|
|
1323
|
+
};
|
|
1324
|
+
medir();
|
|
1325
|
+
if (typeof ResizeObserver === "undefined")
|
|
514
1326
|
return;
|
|
515
|
-
const
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
1327
|
+
const ro = new ResizeObserver(medir);
|
|
1328
|
+
for (const e of [barraRef.current, zoomRef.current])
|
|
1329
|
+
if (e)
|
|
1330
|
+
ro.observe(e);
|
|
1331
|
+
return () => ro.disconnect();
|
|
1332
|
+
}, [editavel, controls]);
|
|
1333
|
+
const margem = React.useMemo(() => reserva ? { top: `${reserva}px`, right: MARGEM_DO_MOTOR, bottom: MARGEM_DO_MOTOR, left: MARGEM_DO_MOTOR } : MARGEM_DO_MOTOR, [reserva]);
|
|
1334
|
+
const caber = React.useCallback((duration = duracao()) => void rf.fitView({ padding: margem, duration }), [rf, margem]);
|
|
1335
|
+
// O primeiro "caber" é o do motor, antes de a reserva existir: quando os nós estão medidos e a reserva
|
|
1336
|
+
// chega, ele cabe de novo, uma vez. Depois disso, a barra que aparece ou some (ligar o modo de arrumar)
|
|
1337
|
+
// DESCE o mapa o tanto dela, sem mexer no zoom que a pessoa escolheu.
|
|
1338
|
+
const medidos = useNodesInitialized();
|
|
1339
|
+
const reservaAnterior = React.useRef(null);
|
|
1340
|
+
React.useEffect(() => {
|
|
1341
|
+
if (!medidos)
|
|
1342
|
+
return;
|
|
1343
|
+
const antes = reservaAnterior.current;
|
|
1344
|
+
reservaAnterior.current = reserva;
|
|
1345
|
+
if (antes === null) {
|
|
1346
|
+
if (reserva)
|
|
1347
|
+
requestAnimationFrame(() => caber(0));
|
|
1348
|
+
return;
|
|
1349
|
+
}
|
|
1350
|
+
if (reserva !== antes) {
|
|
1351
|
+
const v = rf.getViewport();
|
|
1352
|
+
void rf.setViewport({ ...v, y: v.y + reserva - antes });
|
|
1353
|
+
}
|
|
1354
|
+
}, [medidos, reserva, caber, rf]);
|
|
1355
|
+
// A arrumação chegou (ou a pessoa pediu "Reorganizar"): o mapa cabe de novo na tela.
|
|
1356
|
+
React.useEffect(() => { if (pedidoDeCaber)
|
|
1357
|
+
requestAnimationFrame(() => caber()); }, [pedidoDeCaber, caber]);
|
|
521
1358
|
React.useEffect(() => { if (focusId)
|
|
522
1359
|
centralizar(focusId); }, [focusId, centralizar]);
|
|
523
1360
|
// AS SETAS (MNT-19.4): o nó mais perto na direção da seta, dentro de um cone de 45°. O foco vai
|
|
524
|
-
// para o botão dele; se ele estiver fora da tela, a tela vai até ele.
|
|
1361
|
+
// para o botão dele; se ele estiver fora da tela, a tela vai até ele. (Rodada 2) Com `visibleOnly`
|
|
1362
|
+
// o nó de fora da tela nem existe ainda: a tela vai até ele primeiro, e o foco vem no quadro seguinte.
|
|
525
1363
|
React.useEffect(() => {
|
|
526
1364
|
teclado.current = (id, tecla) => {
|
|
527
|
-
const
|
|
528
|
-
|
|
529
|
-
if (!atual)
|
|
1365
|
+
const a = centro(id);
|
|
1366
|
+
if (!a)
|
|
530
1367
|
return;
|
|
531
|
-
const a = centro(atual);
|
|
532
1368
|
let melhor = null;
|
|
533
|
-
|
|
1369
|
+
const nos = rf.getNodes();
|
|
1370
|
+
for (const n of nos) {
|
|
534
1371
|
if (n.id === id || n.hidden)
|
|
535
1372
|
continue;
|
|
536
|
-
|
|
1373
|
+
// O contêiner FECHADO é alvo das setas — fechado, ele é uma caixa só, e o botão dele abre. O aberto
|
|
1374
|
+
// não: o centro dele é o meio da caixa grande, e a seta pularia os nós de dentro.
|
|
1375
|
+
if (n.type === "aureaGrupo" ? nos.some(f => f.parentId === n.id && !f.hidden) : n.type !== "aurea")
|
|
1376
|
+
continue;
|
|
1377
|
+
const b = centro(n.id);
|
|
1378
|
+
if (!b)
|
|
1379
|
+
continue;
|
|
1380
|
+
const dx = b.x - a.x, dy = b.y - a.y;
|
|
537
1381
|
const naDirecao = tecla === "ArrowRight" ? dx > 0 && Math.abs(dy) <= dx : tecla === "ArrowLeft" ? dx < 0 && Math.abs(dy) <= -dx
|
|
538
1382
|
: tecla === "ArrowDown" ? dy > 0 && Math.abs(dx) <= dy : dy < 0 && Math.abs(dx) <= -dy;
|
|
539
1383
|
if (!naDirecao)
|
|
@@ -544,25 +1388,74 @@ function Comandos({ raiz, teclado, focusId, posicoesProntas, controls, apiRef, i
|
|
|
544
1388
|
}
|
|
545
1389
|
if (!melhor)
|
|
546
1390
|
return;
|
|
547
|
-
const alvo = raiz.current
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
1391
|
+
const alvo = () => ancoraDoNo(raiz.current, melhor.id);
|
|
1392
|
+
const botao = alvo();
|
|
1393
|
+
if (!botao) {
|
|
1394
|
+
centralizar(melhor.id);
|
|
1395
|
+
requestAnimationFrame(() => alvo()?.focus({ preventScroll: true }));
|
|
1396
|
+
return;
|
|
1397
|
+
}
|
|
1398
|
+
botao.focus({ preventScroll: true });
|
|
1399
|
+
const caixa = raiz.current?.getBoundingClientRect(), noCaixa = botao.getBoundingClientRect();
|
|
1400
|
+
if (caixa && (noCaixa.left < caixa.left || noCaixa.right > caixa.right || noCaixa.top < caixa.top || noCaixa.bottom > caixa.bottom))
|
|
551
1401
|
centralizar(melhor.id);
|
|
552
1402
|
};
|
|
553
|
-
}, [rf, raiz, teclado, centralizar]);
|
|
1403
|
+
}, [rf, raiz, teclado, centralizar, centro]);
|
|
1404
|
+
// MNT-12.5 e 19.1: longe, o texto das linhas some (o atributo é da raiz, e a regra mora no core).
|
|
1405
|
+
const longe = useStore(st => st.transform[2] < ESCALA_DOS_ROTULOS);
|
|
1406
|
+
React.useEffect(() => { const el = raiz.current; if (!el)
|
|
1407
|
+
return; if (longe)
|
|
1408
|
+
el.setAttribute("data-far", "");
|
|
1409
|
+
else
|
|
1410
|
+
el.removeAttribute("data-far"); }, [longe, raiz]);
|
|
1411
|
+
// E o rótulo que encosta noutro some — depois de cada desenho novo e de cada parada da câmera
|
|
1412
|
+
// (com `visibleOnly`, o rótulo que entra na tela só existe depois de ela andar).
|
|
1413
|
+
const [vista, setVista] = React.useState(0);
|
|
1414
|
+
useOnViewportChange({ onEnd: () => setVista(v => v + 1) });
|
|
1415
|
+
React.useEffect(() => { const q = requestAnimationFrame(() => esconderRotulos(raiz.current)); return () => cancelAnimationFrame(q); }, [desenho, vista, raiz]);
|
|
1416
|
+
// OS ATALHOS do modo de arrumar, como no draw.io: Ctrl+Z desfaz; Ctrl+Shift+Z ou Ctrl+Y refaz; Ctrl+A
|
|
1417
|
+
// escolhe todos. (Cmd no Mac.) Só com o foco dentro do mapa.
|
|
1418
|
+
React.useEffect(() => {
|
|
1419
|
+
const el = raiz.current;
|
|
1420
|
+
if (!editavel || !el)
|
|
1421
|
+
return;
|
|
1422
|
+
const atalho = (ev) => {
|
|
1423
|
+
if (!(ev.ctrlKey || ev.metaKey) || ev.altKey)
|
|
1424
|
+
return;
|
|
1425
|
+
const t = ev.target;
|
|
1426
|
+
if (t && (t.isContentEditable || /^(INPUT|TEXTAREA|SELECT)$/.test(t.tagName)))
|
|
1427
|
+
return;
|
|
1428
|
+
const k = ev.key.toLowerCase();
|
|
1429
|
+
if (k === "z" && !ev.shiftKey) {
|
|
1430
|
+
ev.preventDefault();
|
|
1431
|
+
acoes.current.desfazer();
|
|
1432
|
+
}
|
|
1433
|
+
else if ((k === "z" && ev.shiftKey) || k === "y") {
|
|
1434
|
+
ev.preventDefault();
|
|
1435
|
+
acoes.current.refazer();
|
|
1436
|
+
}
|
|
1437
|
+
else if (k === "a") {
|
|
1438
|
+
ev.preventDefault();
|
|
1439
|
+
acoes.current.escolherTodos();
|
|
1440
|
+
}
|
|
1441
|
+
};
|
|
1442
|
+
el.addEventListener("keydown", atalho);
|
|
1443
|
+
return () => el.removeEventListener("keydown", atalho);
|
|
1444
|
+
}, [editavel, raiz, acoes]);
|
|
554
1445
|
React.useImperativeHandle(apiRef, () => ({
|
|
555
|
-
fitView: () =>
|
|
1446
|
+
fitView: () => caber(),
|
|
556
1447
|
zoomIn: () => { void rf.zoomIn({ duration: duracao() }); },
|
|
557
1448
|
zoomOut: () => { void rf.zoomOut({ duration: duracao() }); },
|
|
558
1449
|
focus: centralizar,
|
|
559
|
-
toDrawio: () => paraDrawio(rf.getNodes(),
|
|
1450
|
+
toDrawio: () => paraDrawio(rf.getNodes(), linhas, forma, titulo),
|
|
560
1451
|
toPng: () => foto(raiz.current, "png"),
|
|
561
1452
|
toSvg: () => foto(raiz.current, "svg"),
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
1453
|
+
undo: () => acoes.current.desfazer(), redo: () => acoes.current.refazer(),
|
|
1454
|
+
align: b => acoes.current.alinhar(b), distribute: e => acoes.current.distribuir(e),
|
|
1455
|
+
relayout: () => acoes.current.reorganizar(), getLayout: () => acoes.current.arrumacao(),
|
|
1456
|
+
}), [rf, centralizar, linhas, forma, titulo, raiz, acoes, caber]);
|
|
1457
|
+
const a = acoes.current;
|
|
1458
|
+
return _jsxs(_Fragment, { children: [editavel && _jsx(Panel, { ref: barraRef, position: "top-left", className: "graph-toolbar", children: _jsxs(Toolbar, { label: s.graphArrange, children: [_jsx(ToolbarButton, { className: "btn-icon", "aria-label": s.graphUndo, disabled: !barra.podeDesfazer, onClick: () => a.desfazer(), children: _jsx(Icon, { name: "arrow-u-up-left" }) }), _jsx(ToolbarButton, { className: "btn-icon", "aria-label": s.graphRedo, disabled: !barra.podeRefazer, onClick: () => a.refazer(), children: _jsx(Icon, { name: "arrow-u-up-right" }) }), _jsx(ToolbarSeparator, {}), _jsx(ToolbarButton, { className: "btn-icon", "aria-label": s.graphAlignLeft, disabled: barra.alinhaveis < 2, onClick: () => a.alinhar("left"), children: _jsx(Icon, { name: "align-left" }) }), _jsx(ToolbarButton, { className: "btn-icon", "aria-label": s.graphAlignCenter, disabled: barra.alinhaveis < 2, onClick: () => a.alinhar("center"), children: _jsx(Icon, { name: "align-center-horizontal" }) }), _jsx(ToolbarButton, { className: "btn-icon", "aria-label": s.graphAlignRight, disabled: barra.alinhaveis < 2, onClick: () => a.alinhar("right"), children: _jsx(Icon, { name: "align-right" }) }), _jsx(ToolbarButton, { className: "btn-icon", "aria-label": s.graphAlignTop, disabled: barra.alinhaveis < 2, onClick: () => a.alinhar("top"), children: _jsx(Icon, { name: "align-top" }) }), _jsx(ToolbarButton, { className: "btn-icon", "aria-label": s.graphAlignMiddle, disabled: barra.alinhaveis < 2, onClick: () => a.alinhar("middle"), children: _jsx(Icon, { name: "align-center-vertical" }) }), _jsx(ToolbarButton, { className: "btn-icon", "aria-label": s.graphAlignBottom, disabled: barra.alinhaveis < 2, onClick: () => a.alinhar("bottom"), children: _jsx(Icon, { name: "align-bottom" }) }), _jsx(ToolbarSeparator, {}), _jsx(ToolbarButton, { className: "btn-icon", "aria-label": s.graphDistributeH, disabled: barra.alinhaveis < 3, onClick: () => a.distribuir("horizontal"), children: _jsx(Icon, { name: "arrows-out-line-horizontal" }) }), _jsx(ToolbarButton, { className: "btn-icon", "aria-label": s.graphDistributeV, disabled: barra.alinhaveis < 3, onClick: () => a.distribuir("vertical"), children: _jsx(Icon, { name: "arrows-out-line-vertical" }) }), _jsx(ToolbarSeparator, {}), _jsx(ToolbarButton, { className: "btn-icon", "aria-label": s.graphPin, disabled: !barra.escolhidos, onClick: () => a.fixar(), "aria-pressed": barra.todosFixos, children: _jsx(Icon, { name: "push-pin" }) }), _jsx(ToolbarButton, { className: "btn-icon", "aria-label": s.graphRelayout, disabled: false, onClick: () => a.reorganizar(), children: _jsx(Icon, { name: "tree-structure" }) })] }) }), controls && _jsxs(Panel, { ref: zoomRef, position: "top-right", className: "graph-controls", children: [_jsx(IconButton, { icon: "magnifying-glass-plus", label: s.graphZoomIn, onClick: () => void rf.zoomIn({ duration: duracao() }) }), _jsx(IconButton, { icon: "magnifying-glass-minus", label: s.graphZoomOut, onClick: () => void rf.zoomOut({ duration: duracao() }) }), _jsx(IconButton, { icon: "corners-out", label: s.graphFit, onClick: () => caber() })] })] });
|
|
566
1459
|
}
|
|
567
1460
|
/**
|
|
568
1461
|
* A FOTO do mapa (MNT-19.2), pelo `modern-screenshot` (peer OPCIONAL, MIT), carregado só aqui. Sai
|