@aurea-uds/react 0.30.0 → 0.32.0

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