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