@rendra-ui/web 2.8.0 → 2.9.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.
Files changed (62) hide show
  1. package/README.md +3 -3
  2. package/dist/base.css +1 -1
  3. package/dist/{button-CrSlVIsY.js → button-BcCBrlnt.js} +6 -6
  4. package/dist/{button-CrSlVIsY.js.map → button-BcCBrlnt.js.map} +1 -1
  5. package/dist/chart.js +7 -7
  6. package/dist/cli/auditar.js +1 -1
  7. package/dist/cli/codigos.js +2 -2
  8. package/dist/cli/executar-auditar.js +1 -1
  9. package/dist/cli/help.js +1 -1
  10. package/dist/cli/trocar.js +2 -2
  11. package/dist/cli/typescript-loader.js +1 -1
  12. package/dist/{cn-BlJHpkDs.js → cn-CpdCvlkP.js} +2 -2
  13. package/dist/{cn-BlJHpkDs.js.map → cn-CpdCvlkP.js.map} +1 -1
  14. package/dist/{components-6RpoJ2Od.js → components-Bus_Jh2R.js} +7 -4
  15. package/dist/components-Bus_Jh2R.js.map +1 -0
  16. package/dist/components.css +67 -26
  17. package/dist/document-viewer.js +4 -4
  18. package/dist/{dropdown-menu-BUGKtR6k.js → dropdown-menu-B3L1XSDf.js} +5 -5
  19. package/dist/{dropdown-menu-BUGKtR6k.js.map → dropdown-menu-B3L1XSDf.js.map} +1 -1
  20. package/dist/{brand-feedback-icon-CVux4pNh.js → empty-state-Bcx4M0Me.js} +64 -4
  21. package/dist/empty-state-Bcx4M0Me.js.map +1 -0
  22. package/dist/{features-max-BCIy1Ic-.js → features-max-CoyJ_kuK.js} +1 -1
  23. package/dist/flow.js +2818 -868
  24. package/dist/flow.js.map +1 -1
  25. package/dist/index.js +80 -465
  26. package/dist/index.js.map +1 -1
  27. package/dist/{motion-context-PJPj6X4g.js → motion-context-DWFl2xRr.js} +4 -4
  28. package/dist/{motion-context-PJPj6X4g.js.map → motion-context-DWFl2xRr.js.map} +1 -1
  29. package/dist/{motion-on-Bocf4Smc.js → motion-on-zwsBqYZL.js} +2 -2
  30. package/dist/{motion-on-Bocf4Smc.js.map → motion-on-zwsBqYZL.js.map} +1 -1
  31. package/dist/motion.js +2 -2
  32. package/dist/{rendra-provider-Do_qWjJC.js → rendra-provider-DuIhBatD.js} +3 -3
  33. package/dist/{rendra-provider-Do_qWjJC.js.map → rendra-provider-DuIhBatD.js.map} +1 -1
  34. package/dist/rich-text-editor.js +4 -4
  35. package/dist/router-bridge.js +2 -2
  36. package/dist/{timeline-xrMHeV92.js → toast-C7qYslCp.js} +464 -80
  37. package/dist/toast-C7qYslCp.js.map +1 -0
  38. package/dist/tokens.css +1 -1
  39. package/dist/{tooltip-to0h6aPe.js → tooltip-BSPO-bH4.js} +5 -5
  40. package/dist/{tooltip-to0h6aPe.js.map → tooltip-BSPO-bH4.js.map} +1 -1
  41. package/dist/types/components/ui/flow-config-panels.d.ts +10 -0
  42. package/dist/types/components/ui/flow-editor.d.ts +5 -0
  43. package/dist/types/components/ui/flow-sidebar.d.ts +21 -0
  44. package/dist/types/components/ui/flow-simulation-panel.d.ts +20 -0
  45. package/dist/types/components/ui/flow-viewer.d.ts +27 -10
  46. package/dist/types/flow.d.ts +1 -1
  47. package/dist/types/lib/flow-blocks.d.ts +122 -0
  48. package/dist/types/lib/flow-edit.d.ts +88 -0
  49. package/dist/types/lib/flow-layout.d.ts +3 -0
  50. package/dist/types/lib/flow-sim.d.ts +24 -0
  51. package/dist/types/lib/flow.d.ts +30 -80
  52. package/dist/{use-breakpoint-CM-yeDHJ.js → use-breakpoint-BGmY3ir-.js} +2 -2
  53. package/dist/{use-breakpoint-CM-yeDHJ.js.map → use-breakpoint-BGmY3ir-.js.map} +1 -1
  54. package/dist/{use-motion-presets-I4StrsCi.js → use-motion-presets-CmMLBQLI.js} +3 -3
  55. package/dist/{use-motion-presets-I4StrsCi.js.map → use-motion-presets-CmMLBQLI.js.map} +1 -1
  56. package/dist/widget-grid.js +2 -2
  57. package/package.json +2 -1
  58. package/dist/brand-feedback-icon-CVux4pNh.js.map +0 -1
  59. package/dist/components-6RpoJ2Od.js.map +0 -1
  60. package/dist/empty-state-i4e-LacG.js +0 -67
  61. package/dist/empty-state-i4e-LacG.js.map +0 -1
  62. package/dist/timeline-xrMHeV92.js.map +0 -1
package/dist/flow.js CHANGED
@@ -1,51 +1,31 @@
1
- /*! Rendra Design System v2.8.0 | MIT | © 2026 Bruno Magalhaes | brunomagalhaes.me */
2
- import { t as cn } from "./cn-BlJHpkDs.js";
3
- import { i as useMotionLib } from "./motion-context-PJPj6X4g.js";
4
- import { t as useMotionPresets } from "./use-motion-presets-I4StrsCi.js";
5
- import { B as ActionBar, F as CardHeader, I as CardTitle, M as CardContent, N as CardDescription, R as Modal, d as Drawer, i as Field, j as Card, n as useFillHeight, p as Input, r as Select, t as Timeline, z as Alert } from "./timeline-xrMHeV92.js";
6
- import { t as Button } from "./button-CrSlVIsY.js";
7
- import { n as useBreakpoint } from "./use-breakpoint-CM-yeDHJ.js";
8
- import { CheckCircle2, Clock, Flag, FlaskConical, GitBranch, ListChecks, Maximize2, Play, SquarePen, Trash2, TriangleAlert, X, Zap, ZoomIn, ZoomOut } from "lucide-react";
9
- import { useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
1
+ /*! Rendra Design System v2.9.0 | MIT | © 2026 Bruno Magalhaes | brunomagalhaes.me */
2
+ import { t as cn } from "./cn-CpdCvlkP.js";
3
+ import { i as useMotionLib } from "./motion-context-DWFl2xRr.js";
4
+ import { t as useMotionPresets } from "./use-motion-presets-CmMLBQLI.js";
5
+ import { B as Alert, R as InfoHint, V as ActionBar, a as RadioGroup, b as Input, c as Select, g as Drawer, i as Textarea, l as Field, n as toast, o as useFillHeight, r as Timeline, s as Tabs, v as Checkbox, z as Modal } from "./toast-C7qYslCp.js";
6
+ import { t as Button } from "./button-BcCBrlnt.js";
7
+ import { t as EmptyState } from "./empty-state-Bcx4M0Me.js";
8
+ import { n as useBreakpoint } from "./use-breakpoint-BGmY3ir-.js";
9
+ import { CalendarClock, CheckCircle2, Cog, Copy, Flag, FlaskConical, GitBranch, Hourglass, ListChecks, Mail, Maximize2, MessageCircle, Play, Plus, Search, Send, SquarePen, Trash2, TriangleAlert, UserPlus, Webhook, X, Zap, ZoomIn, ZoomOut } from "lucide-react";
10
+ import { createContext, useCallback, useContext, useEffect, useId, useMemo, useRef, useState } from "react";
10
11
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
11
- import { ConnectionLineType, Handle, MarkerType, Position, ReactFlow, ReactFlowProvider, applyEdgeChanges, applyNodeChanges, useReactFlow, useStore } from "@xyflow/react";
12
+ import { BaseEdge, ConnectionLineType, EdgeLabelRenderer, Handle, MarkerType, Position, ReactFlow, ReactFlowProvider, applyEdgeChanges, applyNodeChanges, getBezierPath, getNodesBounds, useReactFlow, useStore, useUpdateNodeInternals } from "@xyflow/react";
12
13
  import "@xyflow/react/dist/base.css";
13
- //#region src/lib/flow.ts
14
- var FLOW_NODE_TYPES = [
15
- "gatilho",
16
- "acao",
17
- "condicao",
18
- "espera",
19
- "fim"
20
- ];
14
+ //#region src/lib/flow-blocks.ts
15
+ /** Altura mínima de uma linha de saída da condição em px (alvo de toque, `min-h-touch`). */
16
+ var ROW_H = 44;
21
17
  var FLOW_WAIT_UNITS = [
22
18
  "segundos",
23
19
  "minutos",
24
20
  "horas",
25
21
  "dias"
26
22
  ];
27
- /** Nome do tipo de bloco, como aparece na interface. */
28
- var FLOW_TYPE_NAME = {
29
- gatilho: "Gatilho",
30
- acao: "Ação",
31
- condicao: "Condição",
32
- espera: "Espera",
33
- fim: "Fim"
34
- };
35
- var BRANCH_NAME = {
36
- sim: "Sim",
37
- nao: "Não"
38
- };
39
23
  var WAIT_SINGULAR = {
40
24
  segundos: "segundo",
41
25
  minutos: "minuto",
42
26
  horas: "hora",
43
27
  dias: "dia"
44
28
  };
45
- var DEFAULT_WAIT = {
46
- duration: 5,
47
- unit: "minutos"
48
- };
49
29
  var isUnit = (v) => typeof v === "string" && FLOW_WAIT_UNITS.includes(v);
50
30
  /** A duração do bloco de espera ("5 minutos"), ou `null` se o config não a define. */
51
31
  function formatWait(config) {
@@ -59,23 +39,689 @@ function describeWait(config) {
59
39
  const wait = formatWait(config);
60
40
  return wait ? `Aguardaria ${wait}` : "Aguardaria o tempo definido no bloco";
61
41
  }
42
+ var none = () => ({});
43
+ var FLOW_BLOCKS = {
44
+ "webhook-entrada": {
45
+ type: "webhook-entrada",
46
+ group: "gatilhos",
47
+ name: "Receber webhook",
48
+ description: "Inicia o fluxo quando outro sistema envia dados.",
49
+ hint: "dados de outro sistema, endereço e mapeamento de campos",
50
+ defaultConfig: () => ({
51
+ method: "POST",
52
+ path: "/hooks/novo-evento",
53
+ responseCode: 200,
54
+ received: false,
55
+ mapping: []
56
+ }),
57
+ required: [{
58
+ key: "path",
59
+ label: "Endereço"
60
+ }]
61
+ },
62
+ agendamento: {
63
+ type: "agendamento",
64
+ group: "gatilhos",
65
+ name: "Agendamento",
66
+ description: "Inicia o fluxo em dia e horário definidos.",
67
+ hint: "diário, semanal ou mensal, em um horário",
68
+ defaultConfig: () => ({
69
+ frequency: "diario",
70
+ time: "09:00"
71
+ }),
72
+ required: [{
73
+ key: "time",
74
+ label: "Horário"
75
+ }]
76
+ },
77
+ "registro-criado": {
78
+ type: "registro-criado",
79
+ group: "gatilhos",
80
+ name: "Registro criado",
81
+ description: "Inicia o fluxo quando um registro novo é criado.",
82
+ hint: "cliente, pedido ou negócio criado",
83
+ defaultConfig: () => ({ record: "cliente" }),
84
+ required: []
85
+ },
86
+ whatsapp: {
87
+ type: "whatsapp",
88
+ group: "mensagens",
89
+ name: "Enviar WhatsApp",
90
+ description: "Envia uma mensagem de WhatsApp.",
91
+ hint: "mensagem de texto para um telefone",
92
+ defaultConfig: () => ({
93
+ to: "",
94
+ message: ""
95
+ }),
96
+ required: [{
97
+ key: "to",
98
+ label: "Para"
99
+ }, {
100
+ key: "message",
101
+ label: "Mensagem"
102
+ }]
103
+ },
104
+ email: {
105
+ type: "email",
106
+ group: "mensagens",
107
+ name: "Enviar e-mail",
108
+ description: "Envia um e-mail.",
109
+ hint: "mensagem com assunto para um endereço",
110
+ defaultConfig: () => ({
111
+ to: "",
112
+ subject: "",
113
+ message: ""
114
+ }),
115
+ required: [
116
+ {
117
+ key: "to",
118
+ label: "Para"
119
+ },
120
+ {
121
+ key: "subject",
122
+ label: "Assunto"
123
+ },
124
+ {
125
+ key: "message",
126
+ label: "Mensagem"
127
+ }
128
+ ]
129
+ },
130
+ condicao: {
131
+ type: "condicao",
132
+ group: "condicionais",
133
+ name: "Condição",
134
+ description: "Escolhe o caminho pelas regras.",
135
+ hint: "regras com campo, operador e valor, e a saída Senão",
136
+ defaultConfig: () => ({ rules: [{
137
+ id: "r1",
138
+ field: "",
139
+ operator: "igual",
140
+ value: ""
141
+ }] }),
142
+ required: []
143
+ },
144
+ espera: {
145
+ type: "espera",
146
+ group: "condicionais",
147
+ name: "Esperar",
148
+ description: "Aguarda um tempo antes de seguir.",
149
+ hint: "pausa por segundos, minutos, horas ou dias",
150
+ defaultConfig: () => ({
151
+ duration: 5,
152
+ unit: "minutos"
153
+ }),
154
+ required: []
155
+ },
156
+ "webhook-saida": {
157
+ type: "webhook-saida",
158
+ group: "integracoes",
159
+ name: "Enviar webhook",
160
+ description: "Envia dados para outro sistema.",
161
+ hint: "requisição com corpo montado das variáveis",
162
+ defaultConfig: () => ({
163
+ method: "POST",
164
+ url: "",
165
+ headers: [],
166
+ body: []
167
+ }),
168
+ required: [{
169
+ key: "url",
170
+ label: "Endereço"
171
+ }]
172
+ },
173
+ tarefa: {
174
+ type: "tarefa",
175
+ group: "acoes",
176
+ name: "Criar tarefa",
177
+ description: "Cria uma tarefa para uma pessoa.",
178
+ hint: "título, responsável e prazo em dias",
179
+ defaultConfig: () => ({
180
+ title: "",
181
+ assignee: "",
182
+ dueDays: 1
183
+ }),
184
+ required: [{
185
+ key: "title",
186
+ label: "Título"
187
+ }]
188
+ },
189
+ gatilho: {
190
+ type: "gatilho",
191
+ group: "gatilhos",
192
+ name: "Gatilho personalizado",
193
+ description: "Gatilho sem configuração.",
194
+ hint: "",
195
+ defaultConfig: none,
196
+ required: [],
197
+ hidden: true
198
+ },
199
+ acao: {
200
+ type: "acao",
201
+ group: "acoes",
202
+ name: "Ação personalizada",
203
+ description: "Ação sem configuração.",
204
+ hint: "",
205
+ defaultConfig: none,
206
+ required: [],
207
+ hidden: true
208
+ },
209
+ fim: {
210
+ type: "fim",
211
+ group: "acoes",
212
+ name: "Fim",
213
+ description: "Encerra o caminho.",
214
+ hint: "",
215
+ defaultConfig: none,
216
+ required: [],
217
+ hidden: true
218
+ }
219
+ };
220
+ var FLOW_BLOCK_GROUPS = [
221
+ {
222
+ id: "gatilhos",
223
+ name: "Gatilhos",
224
+ types: [
225
+ "webhook-entrada",
226
+ "agendamento",
227
+ "registro-criado"
228
+ ]
229
+ },
230
+ {
231
+ id: "mensagens",
232
+ name: "Mensagens",
233
+ types: ["whatsapp", "email"]
234
+ },
235
+ {
236
+ id: "condicionais",
237
+ name: "Condicionais",
238
+ types: ["condicao", "espera"]
239
+ },
240
+ {
241
+ id: "integracoes",
242
+ name: "Integrações",
243
+ types: ["webhook-saida"]
244
+ },
245
+ {
246
+ id: "acoes",
247
+ name: "Ações internas",
248
+ types: ["tarefa"]
249
+ }
250
+ ];
251
+ /** Nome do tipo de bloco, como aparece na interface. */
252
+ var FLOW_TYPE_NAME = Object.fromEntries(Object.keys(FLOW_BLOCKS).map((t) => [t, FLOW_BLOCKS[t].name]));
253
+ /** Gatilho é todo bloco do grupo Gatilhos (inclui o legado `gatilho`). */
254
+ var isTrigger = (type) => FLOW_BLOCKS[type].group === "gatilhos";
255
+ var FLOW_SYSTEM_FIELDS = [
256
+ {
257
+ value: "cliente.nome",
258
+ label: "Cliente › Nome"
259
+ },
260
+ {
261
+ value: "cliente.email",
262
+ label: "Cliente › E-mail"
263
+ },
264
+ {
265
+ value: "cliente.telefone",
266
+ label: "Cliente › Telefone"
267
+ },
268
+ {
269
+ value: "cliente.plano",
270
+ label: "Cliente › Plano"
271
+ },
272
+ {
273
+ value: "negocio.valor",
274
+ label: "Negócio › Valor"
275
+ },
276
+ {
277
+ value: "pedido.id",
278
+ label: "Pedido › Número"
279
+ }
280
+ ];
281
+ /** O JSON de exemplo que "chega" no Receber webhook (simulado). */
282
+ var FLOW_SAMPLE_PAYLOAD = {
283
+ cliente: {
284
+ nome: "Ana Souza",
285
+ email: "ana@exemplo.com",
286
+ telefone: "+55 11 98888-0000",
287
+ plano: "pro"
288
+ },
289
+ pedido: {
290
+ id: "PED-1042",
291
+ valor: 1250,
292
+ criado_em: "2026-10-24"
293
+ }
294
+ };
295
+ /** Achata o JSON em caminhos (`cliente.nome`) e valores em texto, na ordem de leitura. */
296
+ function flattenPayload(payload, prefix = "") {
297
+ if (payload && typeof payload === "object" && !Array.isArray(payload)) return Object.entries(payload).flatMap(([k, v]) => flattenPayload(v, prefix ? `${prefix}.${k}` : k));
298
+ return [{
299
+ path: prefix,
300
+ value: String(payload)
301
+ }];
302
+ }
303
+ var SAMPLE_VALUES = {
304
+ ...Object.fromEntries(flattenPayload(FLOW_SAMPLE_PAYLOAD).map((e) => [e.path, e.value])),
305
+ "negocio.titulo": "Proposta Plano Pro",
306
+ "negocio.valor": "1250",
307
+ "agendamento.data": "24/10/2026"
308
+ };
309
+ var RECORD_VARIABLES = {
310
+ cliente: [
311
+ "cliente.nome",
312
+ "cliente.email",
313
+ "cliente.telefone"
314
+ ],
315
+ pedido: [
316
+ "pedido.id",
317
+ "pedido.valor",
318
+ "cliente.nome"
319
+ ],
320
+ negocio: [
321
+ "negocio.titulo",
322
+ "negocio.valor",
323
+ "cliente.nome"
324
+ ]
325
+ };
326
+ var VARIABLE_LABEL = {
327
+ ...Object.fromEntries(FLOW_SYSTEM_FIELDS.map((f) => [f.value, f.label])),
328
+ "pedido.valor": "Pedido › Valor",
329
+ "negocio.titulo": "Negócio › Título",
330
+ "agendamento.data": "Agendamento › Data"
331
+ };
332
+ var asList = (v) => Array.isArray(v) ? v.filter((x) => !!x && typeof x === "object") : [];
333
+ /** Variáveis que os blocos podem usar: as do gatilho do fluxo (sem escopo por caminho). */
334
+ function flowVariables(flow) {
335
+ const trigger = flow.nodes.find((n) => isTrigger(n.type));
336
+ if (!trigger) return [];
337
+ const cfg = trigger.data.config;
338
+ if (trigger.type === "webhook-entrada") return asList(cfg?.mapping).filter((m) => typeof m.from === "string" && typeof m.to === "string" && m.to !== "").map((m) => ({
339
+ id: m.from,
340
+ label: VARIABLE_LABEL[m.to] ?? m.to
341
+ }));
342
+ if (trigger.type === "registro-criado") return (RECORD_VARIABLES[String(cfg?.record)] ?? RECORD_VARIABLES.cliente).map((id) => ({
343
+ id,
344
+ label: VARIABLE_LABEL[id] ?? id
345
+ }));
346
+ if (trigger.type === "agendamento") return [{
347
+ id: "agendamento.data",
348
+ label: VARIABLE_LABEL["agendamento.data"]
349
+ }];
350
+ return [];
351
+ }
352
+ /** Valores de exemplo de cada variável do fluxo (usados no teste e na simulação). */
353
+ function flowSampleData(flow) {
354
+ return Object.fromEntries(flowVariables(flow).map((v) => [v.id, SAMPLE_VALUES[v.id] ?? ""]));
355
+ }
356
+ var VARIABLE = /\{\{\s*([\w.]+)\s*\}\}/g;
357
+ /** Parte o texto em trechos e variáveis `{{x}}` (as variáveis viram chips no card). */
358
+ function splitVariables(text) {
359
+ const parts = [];
360
+ let last = 0;
361
+ for (const m of text.matchAll(VARIABLE)) {
362
+ if (m.index > last) parts.push({
363
+ kind: "text",
364
+ value: text.slice(last, m.index)
365
+ });
366
+ parts.push({
367
+ kind: "var",
368
+ value: m[1]
369
+ });
370
+ last = m.index + m[0].length;
371
+ }
372
+ if (last < text.length) parts.push({
373
+ kind: "text",
374
+ value: text.slice(last)
375
+ });
376
+ return parts;
377
+ }
378
+ /** Troca cada `{{x}}` pelo valor de exemplo; variável sem valor fica como está. */
379
+ function renderTemplate(text, data) {
380
+ return text.replace(VARIABLE, (whole, id) => data[id] ?? whole);
381
+ }
382
+ var FLOW_RULE_OPERATORS = [
383
+ {
384
+ value: "igual",
385
+ label: "é igual a",
386
+ symbol: "="
387
+ },
388
+ {
389
+ value: "diferente",
390
+ label: "é diferente de",
391
+ symbol: "≠"
392
+ },
393
+ {
394
+ value: "maior",
395
+ label: "é maior que",
396
+ symbol: ">"
397
+ },
398
+ {
399
+ value: "menor",
400
+ label: "é menor que",
401
+ symbol: "<"
402
+ },
403
+ {
404
+ value: "contem",
405
+ label: "contém",
406
+ symbol: "contém"
407
+ },
408
+ {
409
+ value: "vazio",
410
+ label: "está vazio",
411
+ symbol: "vazio"
412
+ },
413
+ {
414
+ value: "nao-vazio",
415
+ label: "não está vazio",
416
+ symbol: "preenchido"
417
+ }
418
+ ];
419
+ /** Operadores que dispensam o valor. */
420
+ var operatorHasValue = (op) => op !== "vazio" && op !== "nao-vazio";
421
+ var operatorOf = (op) => FLOW_RULE_OPERATORS.find((o) => o.value === op);
422
+ var isRuleComplete = (rule) => rule.field.trim() !== "" && !!operatorOf(rule.operator) && (!operatorHasValue(rule.operator) || rule.value.trim() !== "");
423
+ /** "pedido.valor é maior que 500". */
424
+ function ruleSummary(rule) {
425
+ if (!isRuleComplete(rule)) return "Regra incompleta";
426
+ const op = operatorOf(rule.operator).label;
427
+ return operatorHasValue(rule.operator) ? `${rule.field} ${op} ${rule.value}` : `${rule.field} ${op}`;
428
+ }
429
+ /** As regras do bloco condição (lista vazia se o config não as traz). */
430
+ function rulesOf(node) {
431
+ return asList(node.data.config?.rules).map((r) => ({
432
+ id: String(r.id ?? ""),
433
+ ...typeof r.label === "string" ? { label: r.label } : {},
434
+ field: String(r.field ?? ""),
435
+ operator: operatorOf(r.operator) ? r.operator : "igual",
436
+ value: String(r.value ?? "")
437
+ }));
438
+ }
439
+ /** Nome da saída: o rótulo da regra (ou "Regra N") e "Senão". */
440
+ function branchName(node, branch) {
441
+ if (branch === "senao") return "Senão";
442
+ const rules = rulesOf(node);
443
+ const i = rules.findIndex((r) => r.id === branch);
444
+ return i < 0 ? branch : rules[i].label || `Regra ${i + 1}`;
445
+ }
446
+ /** Menor `rN` livre para uma regra nova. */
447
+ function nextRuleId(rules) {
448
+ const taken = new Set(rules.map((r) => r.id));
449
+ let n = 1;
450
+ while (taken.has(`r${n}`)) n++;
451
+ return `r${n}`;
452
+ }
453
+ /** Texto curto na ligação que sai de uma condição ("> 500", "= pro", "Senão"). */
454
+ function edgeLabel(flow, edge) {
455
+ const from = flow.nodes.find((n) => n.id === edge.source);
456
+ if (!from || from.type !== "condicao" || !edge.branch) return null;
457
+ if (edge.branch === "senao") return "Senão";
458
+ const rule = rulesOf(from).find((r) => r.id === edge.branch);
459
+ if (!rule) return null;
460
+ if (!isRuleComplete(rule)) return branchName(from, edge.branch);
461
+ const symbol = operatorOf(rule.operator).symbol;
462
+ return operatorHasValue(rule.operator) ? `${symbol} ${rule.value}` : symbol;
463
+ }
464
+ /** Saídas de um bloco: as regras e "Senão" na condição; uma só nos demais. */
465
+ function outputIds(node) {
466
+ return node.type === "condicao" ? [...rulesOf(node).map((r) => r.id), "senao"] : [void 0];
467
+ }
468
+ var FREQUENCY$1 = {
469
+ diario: "Diário",
470
+ semanal: "Semanal",
471
+ mensal: "Mensal"
472
+ };
473
+ var RECORD_LABEL = {
474
+ cliente: "Cliente",
475
+ pedido: "Pedido",
476
+ negocio: "Negócio"
477
+ };
478
+ /** O texto, ou vazio quando o valor não é texto. */
479
+ var str = (v) => typeof v === "string" ? v : "";
480
+ /** Linhas "rótulo e valor" do resumo no card (variáveis `{{x}}` viram chips). */
481
+ function summarizeConfig(node) {
482
+ const c = node.data.config;
483
+ switch (node.type) {
484
+ case "webhook-entrada": {
485
+ const mapped = asList(c?.mapping).filter((m) => str(m.to) !== "").map((m) => `{{${str(m.from)}}}`).join(" ");
486
+ return [{
487
+ label: "Método e endereço",
488
+ value: `${str(c?.method) || "POST"} ${str(c?.path)}`.trim()
489
+ }, {
490
+ label: "Campos mapeados",
491
+ value: mapped
492
+ }];
493
+ }
494
+ case "agendamento": return [{
495
+ label: "Frequência",
496
+ value: FREQUENCY$1[str(c?.frequency)] ?? ""
497
+ }, {
498
+ label: "Horário",
499
+ value: str(c?.time)
500
+ }];
501
+ case "registro-criado": return [{
502
+ label: "Registro",
503
+ value: RECORD_LABEL[str(c?.record)] ?? ""
504
+ }];
505
+ case "whatsapp": return [{
506
+ label: "Para",
507
+ value: str(c?.to)
508
+ }, {
509
+ label: "Mensagem",
510
+ value: str(c?.message)
511
+ }];
512
+ case "email": return [
513
+ {
514
+ label: "Para",
515
+ value: str(c?.to)
516
+ },
517
+ {
518
+ label: "Assunto",
519
+ value: str(c?.subject)
520
+ },
521
+ {
522
+ label: "Mensagem",
523
+ value: str(c?.message)
524
+ }
525
+ ];
526
+ case "espera": return [{
527
+ label: "Tempo",
528
+ value: formatWait(c) ?? ""
529
+ }];
530
+ case "webhook-saida": return [{
531
+ label: "Método e endereço",
532
+ value: `${str(c?.method) || "POST"} ${str(c?.url)}`.trim()
533
+ }, {
534
+ label: "Corpo",
535
+ value: asList(c?.body).map((b) => str(b.value)).filter(Boolean).join(" ")
536
+ }];
537
+ case "tarefa": {
538
+ const days = typeof c?.dueDays === "number" ? c.dueDays : 0;
539
+ return [
540
+ {
541
+ label: "Título",
542
+ value: str(c?.title)
543
+ },
544
+ {
545
+ label: "Responsável",
546
+ value: str(c?.assignee)
547
+ },
548
+ {
549
+ label: "Prazo",
550
+ value: `${days} ${days === 1 ? "dia" : "dias"}`
551
+ }
552
+ ];
553
+ }
554
+ default: return [];
555
+ }
556
+ }
557
+ /** Requisição simulada do Enviar webhook, com os valores de exemplo no corpo. */
558
+ function buildWebhookRequest(config, data) {
559
+ const body = {};
560
+ for (const b of asList(config?.body)) if (str(b.key) !== "") body[str(b.key)] = renderTemplate(str(b.value), data);
561
+ return {
562
+ method: str(config?.method) || "POST",
563
+ url: str(config?.url),
564
+ body
565
+ };
566
+ }
567
+ /** Linhas que o valor do resumo ocupa no card (chips largos quebram em mais de uma). */
568
+ function valueLines(value) {
569
+ let width = 0;
570
+ for (const part of splitVariables(value)) width += part.kind === "var" ? (part.value.length + 4) * 6.6 + 20 : part.value.length * 6.4;
571
+ return Math.max(1, Math.ceil(width / 290));
572
+ }
573
+ /**
574
+ * Altura estimada do card em px (medida no navegador e arredondada para cima), usada enquanto o
575
+ * React Flow ainda não mediu o bloco: na arrumação automática e ao procurar espaço livre.
576
+ */
577
+ function estimateNodeHeight(node) {
578
+ if (node.type === "condicao") return 88 + ROW_H * (rulesOf(node).length + 1);
579
+ const rows = summarizeConfig(node);
580
+ let h = 86;
581
+ if (rows.length > 0) h += 25 + rows.reduce((sum, r) => sum + 20 + 24 * valueLines(r.value), 0) + 8 * (rows.length - 1);
582
+ if (node.type === "webhook-entrada" && node.data.config?.received === true) h += 44;
583
+ return h;
584
+ }
585
+ //#endregion
586
+ //#region src/lib/flow-layout.ts
587
+ /** Camada de cada bloco (maior distância do início sem passar pelas ligações que fecham ciclo). */
588
+ function layoutFlow(flow) {
589
+ const byId = new Map(flow.nodes.map((n) => [n.id, n]));
590
+ const edges = flow.edges.filter((e) => byId.has(e.source) && byId.has(e.target));
591
+ const out = /* @__PURE__ */ new Map();
592
+ for (const n of flow.nodes) {
593
+ const mine = edges.filter((e) => e.source === n.id);
594
+ const order = outputIds(n);
595
+ mine.sort((a, b) => order.indexOf(a.branch) - order.indexOf(b.branch));
596
+ out.set(n.id, mine);
597
+ }
598
+ const hasIncoming = new Set(edges.map((e) => e.target));
599
+ let roots = flow.nodes.filter((n) => isTrigger(n.type)).map((n) => n.id);
600
+ if (roots.length === 0) roots = flow.nodes.filter((n) => !hasIncoming.has(n.id)).map((n) => n.id);
601
+ if (roots.length === 0 && flow.nodes[0]) roots = [flow.nodes[0].id];
602
+ const state = /* @__PURE__ */ new Map();
603
+ const preorder = /* @__PURE__ */ new Map();
604
+ const post = [];
605
+ const back = /* @__PURE__ */ new Set();
606
+ const visit = (id) => {
607
+ state.set(id, 1);
608
+ preorder.set(id, preorder.size);
609
+ for (const e of out.get(id) ?? []) if (state.get(e.target) === 1) back.add(e.id);
610
+ else if (!state.has(e.target)) visit(e.target);
611
+ state.set(id, 2);
612
+ post.push(id);
613
+ };
614
+ for (const r of roots) if (!state.has(r)) visit(r);
615
+ const layer = new Map(roots.map((r) => [r, 0]));
616
+ for (const id of [...post].reverse()) for (const e of out.get(id) ?? []) if (!back.has(e.id)) layer.set(e.target, Math.max(layer.get(e.target) ?? 0, layer.get(id) + 1));
617
+ const unreachedLayer = Math.max(-1, ...layer.values()) + 1;
618
+ const columns = /* @__PURE__ */ new Map();
619
+ const ordered = [...flow.nodes].sort((a, b) => {
620
+ const pa = preorder.get(a.id) ?? Infinity;
621
+ const pb = preorder.get(b.id) ?? Infinity;
622
+ return pa === pb ? flow.nodes.indexOf(a) - flow.nodes.indexOf(b) : pa - pb;
623
+ });
624
+ for (const n of ordered) {
625
+ const l = layer.get(n.id) ?? unreachedLayer;
626
+ columns.set(l, [...columns.get(l) ?? [], n.id]);
627
+ }
628
+ const position = /* @__PURE__ */ new Map();
629
+ for (const [l, ids] of columns) {
630
+ let y = 0;
631
+ for (const id of ids) {
632
+ position.set(id, {
633
+ x: l * 432,
634
+ y
635
+ });
636
+ y += estimateNodeHeight(byId.get(id)) + 32;
637
+ }
638
+ }
639
+ return {
640
+ ...flow,
641
+ nodes: flow.nodes.map((n) => ({
642
+ ...n,
643
+ position: position.get(n.id)
644
+ }))
645
+ };
646
+ }
647
+ //#endregion
648
+ //#region src/lib/flow.ts
649
+ var V2_TYPES = [
650
+ "webhook-entrada",
651
+ "agendamento",
652
+ "registro-criado",
653
+ "whatsapp",
654
+ "email",
655
+ "condicao",
656
+ "espera",
657
+ "webhook-saida",
658
+ "tarefa",
659
+ "gatilho",
660
+ "acao"
661
+ ];
662
+ var LEGACY_TYPES = [
663
+ "gatilho",
664
+ "acao",
665
+ "condicao",
666
+ "espera",
667
+ "fim"
668
+ ];
62
669
  /** Nome do bloco para mensagens e rótulos: o rótulo, ou o tipo quando ele está vazio. */
63
670
  var nodeName = (n) => n.data.label.trim() || FLOW_TYPE_NAME[n.type];
671
+ function renameEdge(e) {
672
+ return {
673
+ id: `${e.source}:senao->${e.target}`,
674
+ source: e.source,
675
+ target: e.target,
676
+ branch: "senao"
677
+ };
678
+ }
679
+ /**
680
+ * Leva um fluxo da 2.7.0 (sem `version`) ao formato v2: tira os blocos Fim, a condição vira uma
681
+ * regra "Sim" mais "Senão", e os blocos são arrumados da esquerda para a direita. Quem já é v2
682
+ * volta igual.
683
+ */
684
+ function migrateFlow(flow) {
685
+ if (flow.version === 2) return flow;
686
+ const fim = new Set(flow.nodes.filter((n) => n.type === "fim").map((n) => n.id));
687
+ return layoutFlow({
688
+ version: 2,
689
+ nodes: flow.nodes.filter((n) => !fim.has(n.id)).map((n) => n.type === "condicao" && !Array.isArray(n.data.config?.rules) ? {
690
+ ...n,
691
+ data: {
692
+ ...n.data,
693
+ config: {
694
+ ...n.data.config,
695
+ rules: [{
696
+ id: "sim",
697
+ label: "Sim",
698
+ field: "",
699
+ operator: "igual",
700
+ value: ""
701
+ }]
702
+ }
703
+ }
704
+ } : n),
705
+ edges: flow.edges.filter((e) => !fim.has(e.source) && !fim.has(e.target)).map((e) => e.branch === "nao" ? renameEdge(e) : e)
706
+ });
707
+ }
64
708
  /** Ligações que saem de cada bloco, na ordem do fluxo. */
65
709
  function outgoing(flow) {
66
710
  const map = /* @__PURE__ */ new Map();
67
711
  for (const e of flow.edges) map.set(e.source, [...map.get(e.source) ?? [], e]);
68
712
  return map;
69
713
  }
714
+ var filled = (v) => typeof v === "string" && v.trim() !== "";
70
715
  /** Problemas do fluxo, em ordem estável: a mesma entrada dá sempre a mesma lista. */
71
- function validateFlow(flow) {
716
+ function validateFlow(input) {
717
+ const flow = migrateFlow(input);
72
718
  const issues = [];
73
719
  const byId = new Map(flow.nodes.map((n) => [n.id, n]));
74
720
  const out = outgoing(flow);
75
- const triggers = flow.nodes.filter((n) => n.type === "gatilho");
721
+ const triggers = flow.nodes.filter((n) => isTrigger(n.type));
76
722
  if (triggers.length === 0) issues.push({
77
723
  code: "sem-gatilho",
78
- message: "O fluxo precisa de um bloco Gatilho para começar."
724
+ message: "O fluxo precisa de um gatilho para começar."
79
725
  });
80
726
  else if (triggers.length > 1) issues.push({
81
727
  code: "varios-gatilhos",
@@ -93,10 +739,10 @@ function validateFlow(flow) {
93
739
  });
94
740
  continue;
95
741
  }
96
- if (from.type === "condicao" ? !e.branch : e.branch) {
742
+ if (!(from.type === "condicao" ? !!e.branch && (e.branch === "senao" || rulesOf(from).some((r) => r.id === e.branch)) : !e.branch)) {
97
743
  issues.push({
98
744
  code: "ramo-invalido",
99
- message: from.type === "condicao" ? `Uma saída da condição "${nodeName(from)}" não diz se é "Sim" ou "Não".` : `A ligação que sai de "${nodeName(from)}" não pode ter "Sim" ou "Não": isso só vale para condição.`,
745
+ message: from.type === "condicao" ? `Uma saída da condição "${nodeName(from)}" não corresponde a nenhuma regra nem ao "Senão".` : `A ligação que sai de "${nodeName(from)}" não pode ter saída de regra: isso só vale para condição.`,
100
746
  nodeId: from.id,
101
747
  edgeId: e.id
102
748
  });
@@ -107,40 +753,48 @@ function validateFlow(flow) {
107
753
  for (const n of flow.nodes) {
108
754
  const edges = out.get(n.id) ?? [];
109
755
  const name = nodeName(n);
110
- if (n.type === "fim") {
111
- if (edges.length > 0) issues.push({
112
- code: "fim-com-saida",
113
- message: `O bloco Fim "${name}" não pode ter saída.`,
114
- nodeId: n.id
115
- });
116
- } else if (n.type === "condicao") {
117
- const sim = edges.filter((e) => e.branch === "sim").length;
118
- const nao = edges.filter((e) => e.branch === "nao").length;
119
- if (sim === 0) issues.push({
120
- code: "condicao-sem-sim",
121
- message: `A condição "${name}" precisa de uma saída "Sim".`,
756
+ const cfg = n.data.config;
757
+ if (n.type === "condicao") {
758
+ const rules = rulesOf(n);
759
+ if (rules.length === 0) issues.push({
760
+ code: "condicao-sem-regra",
761
+ message: `A condição "${name}" precisa de ao menos uma regra.`,
122
762
  nodeId: n.id
123
763
  });
124
- if (nao === 0) issues.push({
125
- code: "condicao-sem-nao",
126
- message: `A condição "${name}" precisa de uma saída "Não".`,
127
- nodeId: n.id
764
+ rules.forEach((r, i) => {
765
+ if (!isRuleComplete(r)) issues.push({
766
+ code: "regra-incompleta",
767
+ message: `A regra ${i + 1} da condição "${name}" precisa de campo, operador e valor.`,
768
+ nodeId: n.id
769
+ });
128
770
  });
129
- if (sim > 1 || nao > 1) issues.push({
771
+ const seen = /* @__PURE__ */ new Set();
772
+ let repeated = false;
773
+ for (const e of edges) {
774
+ if (!e.branch) continue;
775
+ if (seen.has(e.branch)) repeated = true;
776
+ seen.add(e.branch);
777
+ }
778
+ if (repeated) issues.push({
130
779
  code: "condicao-saida-repetida",
131
- message: `A condição "${name}" tem mais de uma saída para o mesmo ramo.`,
780
+ message: `A condição "${name}" tem mais de uma ligação na mesma saída.`,
132
781
  nodeId: n.id
133
782
  });
134
- } else if (edges.length === 0) issues.push({
135
- code: "sem-saida",
136
- message: `O bloco "${name}" não leva a nenhum próximo bloco.`,
137
- nodeId: n.id
138
- });
139
- else if (edges.length > 1) issues.push({
783
+ } else if (edges.length > 1) issues.push({
140
784
  code: "varias-saidas",
141
785
  message: `O bloco "${name}" tem mais de uma saída; só a condição pode ter.`,
142
786
  nodeId: n.id
143
787
  });
788
+ for (const f of FLOW_BLOCKS[n.type].required) if (!filled(cfg?.[f.key])) issues.push({
789
+ code: "config-obrigatoria-vazia",
790
+ message: `O bloco "${name}" precisa do campo "${f.label}".`,
791
+ nodeId: n.id
792
+ });
793
+ if (n.type === "espera" && !(typeof cfg?.duration === "number" && cfg.duration > 0)) issues.push({
794
+ code: "config-obrigatoria-vazia",
795
+ message: `O bloco "${name}" precisa de uma duração maior que zero.`,
796
+ nodeId: n.id
797
+ });
144
798
  }
145
799
  const next = /* @__PURE__ */ new Map();
146
800
  for (const e of sound) next.set(e.source, [...next.get(e.source) ?? [], e.target]);
@@ -183,8 +837,12 @@ var sortKeys = (v) => {
183
837
  if (v && typeof v === "object") return Object.fromEntries(Object.keys(v).sort().map((k) => [k, sortKeys(v[k])]));
184
838
  return v;
185
839
  };
186
- /** JSON do fluxo, com a ordem das chaves fixa: o mesmo fluxo dá sempre o mesmo texto. */
187
- function serializeFlow(flow) {
840
+ /**
841
+ * JSON do fluxo (sempre no formato v2, com `version` na primeira chave) e a ordem das chaves
842
+ * fixa: o mesmo fluxo dá sempre o mesmo texto. Um fluxo da 2.7.0 é convertido antes.
843
+ */
844
+ function serializeFlow(input) {
845
+ const flow = migrateFlow(input);
188
846
  const nodes = flow.nodes.map((n) => ({
189
847
  id: n.id,
190
848
  type: n.type,
@@ -208,12 +866,16 @@ function serializeFlow(flow) {
208
866
  target: e.target
209
867
  });
210
868
  return JSON.stringify({
869
+ version: 2,
211
870
  nodes,
212
871
  edges
213
872
  }, null, 2);
214
873
  }
215
874
  var isObject = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
216
- /** Lê o JSON de um fluxo. Nunca lança; campos que o contrato não conhece são ignorados. */
875
+ /**
876
+ * Lê o JSON de um fluxo. Nunca lança; campos que o contrato não conhece são ignorados. Texto sem
877
+ * `version` é lido pelas regras da 2.7.0 e convertido; `version: 2` segue as regras do formato novo.
878
+ */
217
879
  function parseFlow(text) {
218
880
  let raw;
219
881
  try {
@@ -228,10 +890,16 @@ function parseFlow(text) {
228
890
  ok: false,
229
891
  errors: ["O JSON do fluxo precisa ser um objeto."]
230
892
  };
893
+ if (raw.version !== void 0 && raw.version !== 2) return {
894
+ ok: false,
895
+ errors: ["Versão do formato não suportada (use 2 ou deixe sem versão)."]
896
+ };
231
897
  if (!Array.isArray(raw.nodes) || !Array.isArray(raw.edges)) return {
232
898
  ok: false,
233
899
  errors: [`O campo "${!Array.isArray(raw.nodes) ? "nodes" : "edges"}" precisa ser uma lista.`]
234
900
  };
901
+ const v2 = raw.version === 2;
902
+ const types = v2 ? V2_TYPES : LEGACY_TYPES;
235
903
  const errors = [];
236
904
  const nodes = [];
237
905
  const ids = /* @__PURE__ */ new Set();
@@ -247,7 +915,7 @@ function parseFlow(text) {
247
915
  errors.push(`${at}: o id "${id}" está repetido.`);
248
916
  ok = false;
249
917
  } else ids.add(id);
250
- if (!FLOW_NODE_TYPES.includes(type)) {
918
+ if (!types.includes(type)) {
251
919
  errors.push(`${at}: tipo desconhecido (${String(type)}).`);
252
920
  ok = false;
253
921
  }
@@ -298,9 +966,11 @@ function parseFlow(text) {
298
966
  errors.push(`${at}: o id "${id}" está repetido.`);
299
967
  ok = false;
300
968
  }
301
- if (branch !== void 0 && branch !== "sim" && branch !== "nao") {
302
- errors.push(`${at}: "branch" só pode ser "sim" ou "nao".`);
303
- ok = false;
969
+ if (branch !== void 0) {
970
+ if (!(v2 ? typeof branch === "string" && branch !== "" : branch === "sim" || branch === "nao")) {
971
+ errors.push(v2 ? `${at}: "branch" precisa ser o id de uma regra ou "senao".` : `${at}: "branch" só pode ser "sim" ou "nao".`);
972
+ ok = false;
973
+ }
304
974
  }
305
975
  if (ok) {
306
976
  edgeIds.add(id);
@@ -316,310 +986,155 @@ function parseFlow(text) {
316
986
  });
317
987
  }
318
988
  });
319
- return errors.length > 0 ? {
989
+ if (errors.length > 0) return {
320
990
  ok: false,
321
991
  errors
322
- } : {
992
+ };
993
+ return {
323
994
  ok: true,
324
- flow: {
995
+ flow: migrateFlow(v2 ? {
996
+ version: 2,
325
997
  nodes,
326
998
  edges
327
- }
999
+ } : {
1000
+ nodes,
1001
+ edges
1002
+ })
328
1003
  };
329
1004
  }
330
- /** Acrescenta um bloco com id único e determinístico (`tipo-N`). */
331
- function addNode(flow, type, position, options = {}) {
332
- const taken = /* @__PURE__ */ new Set([...flow.nodes.map((n) => n.id), ...options.reservedIds ?? []]);
333
- let n = 1;
334
- while (taken.has(`${type}-${n}`)) n++;
335
- const id = `${type}-${n}`;
336
- const node = {
1005
+ //#endregion
1006
+ //#region src/components/ui/flow-viewer.tsx
1007
+ var FlowActionsContext = createContext(null);
1008
+ var FLOW_ICON = {
1009
+ "webhook-entrada": Webhook,
1010
+ agendamento: CalendarClock,
1011
+ "registro-criado": UserPlus,
1012
+ whatsapp: MessageCircle,
1013
+ email: Mail,
1014
+ condicao: GitBranch,
1015
+ espera: Hourglass,
1016
+ "webhook-saida": Send,
1017
+ tarefa: ListChecks,
1018
+ gatilho: Zap,
1019
+ acao: Cog,
1020
+ fim: Flag
1021
+ };
1022
+ var TONE = {
1023
+ gatilhos: "bg-primary-soft text-primary-soft-foreground",
1024
+ mensagens: "bg-success-soft text-success-soft-foreground",
1025
+ condicionais: "bg-warning-soft text-warning-soft-foreground",
1026
+ integracoes: "bg-info-soft text-info-soft-foreground",
1027
+ acoes: "bg-muted text-muted-foreground"
1028
+ };
1029
+ /** Cor do ícone do bloco, por grupo (também usada na barra de blocos). */
1030
+ var flowTone = (type) => TONE[FLOW_BLOCKS[type].group];
1031
+ var DIRECTION = {
1032
+ up: "para cima",
1033
+ down: "para baixo",
1034
+ left: "para a esquerda",
1035
+ right: "para a direita"
1036
+ };
1037
+ /** Os textos que a biblioteca escreve no DOM (leitor de tela), em português. */
1038
+ var FLOW_ARIA = {
1039
+ "node.a11yDescription.default": "Pressione Enter para abrir o bloco, Espaço para selecioná-lo e Delete para apagá-lo. Esc cancela.",
1040
+ "node.a11yDescription.keyboardDisabled": "Pressione Enter para abrir o bloco, Espaço para selecioná-lo e Delete para apagá-lo. Esc cancela.",
1041
+ "node.a11yDescription.ariaLiveMessage": ({ direction, x, y }) => `Bloco movido ${DIRECTION[direction] ?? direction}. Nova posição, x: ${x}, y: ${y}`,
1042
+ "edge.a11yDescription.default": "Pressione Espaço para selecionar a ligação e Delete para apagá-la. Esc cancela."
1043
+ };
1044
+ /** Os blocos do Rendra como nós do React Flow (a biblioteca só vê isto). */
1045
+ function toRfNodes(flow, opts = {}) {
1046
+ const byNode = /* @__PURE__ */ new Map();
1047
+ for (const issue of opts.issues ?? []) if (issue.nodeId) byNode.set(issue.nodeId, [...byNode.get(issue.nodeId) ?? [], issue.message]);
1048
+ return flow.nodes.map((n) => ({
1049
+ id: n.id,
1050
+ type: "bloco",
1051
+ position: n.position,
1052
+ ariaLabel: n.data.label,
1053
+ domAttributes: { "aria-roledescription": "bloco" },
1054
+ data: {
1055
+ label: n.data.label,
1056
+ kind: n.type,
1057
+ ...n.data.config ? { config: n.data.config } : {},
1058
+ issues: byNode.get(n.id) ?? [],
1059
+ active: opts.activeId === n.id
1060
+ }
1061
+ }));
1062
+ }
1063
+ /** As ligações do Rendra como arestas do React Flow, com a descrição em português. */
1064
+ function toRfEdges(flow) {
1065
+ const nameOf = new Map(flow.nodes.map((n) => [n.id, nodeName(n)]));
1066
+ return flow.edges.map((e) => {
1067
+ const label = edgeLabel(flow, e);
1068
+ const from = nameOf.get(e.source) ?? e.source;
1069
+ const to = nameOf.get(e.target) ?? e.target;
1070
+ return {
1071
+ id: e.id,
1072
+ source: e.source,
1073
+ target: e.target,
1074
+ type: "ligacao",
1075
+ animated: true,
1076
+ ...e.branch ? { sourceHandle: e.branch } : {},
1077
+ ariaLabel: `De "${from}" para "${to}"${label ? `, se ${label}` : ""}`,
1078
+ domAttributes: { "aria-roledescription": "ligação" },
1079
+ data: {
1080
+ label,
1081
+ from,
1082
+ to
1083
+ }
1084
+ };
1085
+ });
1086
+ }
1087
+ function Port({ type, id, isConnectable, position = type === "target" ? Position.Left : Position.Right }) {
1088
+ return /* @__PURE__ */ jsx(Handle, {
337
1089
  id,
338
1090
  type,
339
- position: {
340
- x: position.x,
341
- y: position.y
342
- },
343
- data: type === "espera" ? {
344
- label: options.label ?? FLOW_TYPE_NAME[type],
345
- config: { ...DEFAULT_WAIT }
346
- } : { label: options.label ?? FLOW_TYPE_NAME[type] }
347
- };
348
- return {
349
- flow: {
350
- ...flow,
351
- nodes: [...flow.nodes, node]
352
- },
353
- id
354
- };
355
- }
356
- /** Apaga o bloco e as ligações que chegam ou saem dele. */
357
- function removeNode(flow, id) {
358
- if (!flow.nodes.some((n) => n.id === id)) return flow;
359
- return {
360
- nodes: flow.nodes.filter((n) => n.id !== id),
361
- edges: flow.edges.filter((e) => e.source !== id && e.target !== id)
362
- };
1091
+ position,
1092
+ className: "size-touch",
1093
+ isConnectable,
1094
+ children: /* @__PURE__ */ jsx("span", {
1095
+ "aria-hidden": true,
1096
+ "data-flow-dot": true,
1097
+ className: "pointer-events-none absolute inset-0 m-auto size-3 rounded-full border-2 border-card bg-primary-text"
1098
+ })
1099
+ });
363
1100
  }
364
- function removeEdge(flow, id) {
365
- return {
366
- ...flow,
367
- edges: flow.edges.filter((e) => e.id !== id)
368
- };
369
- }
370
- function moveNode(flow, id, position) {
371
- return {
372
- ...flow,
373
- nodes: flow.nodes.map((n) => n.id === id ? {
374
- ...n,
375
- position: {
376
- x: position.x,
377
- y: position.y
378
- }
379
- } : n)
380
- };
381
- }
382
- /** Troca o rótulo e/ou o config do bloco (o config informado substitui o anterior). */
383
- function updateNodeData(flow, id, patch) {
384
- return {
385
- ...flow,
386
- nodes: flow.nodes.map((n) => {
387
- if (n.id !== id) return n;
388
- const label = patch.label ?? n.data.label;
389
- const config = patch.config ?? n.data.config;
390
- return {
391
- ...n,
392
- data: config ? {
393
- label,
394
- config
395
- } : { label }
396
- };
397
- })
398
- };
399
- }
400
- var edgeId = (source, target, branch) => branch ? `${source}:${branch}->${target}` : `${source}->${target}`;
401
- /**
402
- * Liga dois blocos. A segunda saída de um bloco comum (ou do mesmo ramo da condição) substitui
403
- * a primeira. Recusa, com o motivo, o que o fluxo nunca aceita.
404
- */
405
- function connectNodes(flow, source, target, branch) {
406
- const from = flow.nodes.find((n) => n.id === source);
407
- const to = flow.nodes.find((n) => n.id === target);
408
- if (!from || !to) return {
409
- ok: false,
410
- reason: "Esse bloco não existe mais."
411
- };
412
- if (source === target) return {
413
- ok: false,
414
- reason: "Um bloco não pode ligar a ele mesmo."
415
- };
416
- if (from.type === "fim") return {
417
- ok: false,
418
- reason: "O bloco Fim termina o fluxo e não tem saída."
419
- };
420
- if (to.type === "gatilho") return {
421
- ok: false,
422
- reason: "O gatilho começa o fluxo e não recebe ligação."
423
- };
424
- if (from.type === "condicao" && !branch) return {
425
- ok: false,
426
- reason: "Escolha a saída \"Sim\" ou \"Não\" da condição."
427
- };
428
- if (from.type !== "condicao" && branch) return {
429
- ok: false,
430
- reason: "\"Sim\" e \"Não\" só existem na condição."
431
- };
432
- const id = edgeId(source, target, branch);
433
- if (flow.edges.some((e) => e.id === id)) return {
434
- ok: false,
435
- reason: "Essa ligação já existe."
436
- };
437
- const kept = flow.edges.filter((e) => !(e.source === source && (from.type === "condicao" ? e.branch === branch : true)));
438
- const edge = branch ? {
439
- id,
440
- source,
441
- target,
442
- branch
443
- } : {
444
- id,
445
- source,
446
- target
447
- };
448
- return {
449
- ok: true,
450
- flow: {
451
- ...flow,
452
- edges: [...kept, edge]
453
- }
454
- };
455
- }
456
- /**
457
- * Define (ou, com `target` nulo, tira) o próximo bloco de `id`; na condição, o do ramo dado.
458
- * Uma ligação recusada devolve o próprio fluxo, sem mudança.
459
- */
460
- function setNext(flow, id, target, branch) {
461
- if (target === null) return {
462
- ...flow,
463
- edges: flow.edges.filter((e) => !(e.source === id && (branch ? e.branch === branch : true)))
464
- };
465
- const r = connectNodes(flow, id, target, branch);
466
- return r.ok ? r.flow : flow;
467
- }
468
- /** Blocos que `id` pode ter como próximo: nunca ele mesmo nem o gatilho. */
469
- function nextCandidates(flow, id) {
470
- const from = flow.nodes.find((n) => n.id === id);
471
- if (!from || from.type === "fim") return [];
472
- return flow.nodes.filter((n) => n.id !== id && n.type !== "gatilho");
473
- }
474
- function resetSimulation() {
475
- return {
476
- status: "idle",
477
- steps: [],
478
- currentId: null,
479
- issues: []
480
- };
481
- }
482
- /** Começa no gatilho. Fluxo inválido não inicia: devolve `error` com os problemas. */
483
- function startSimulation(flow) {
484
- const issues = validateFlow(flow);
485
- if (issues.length > 0) return {
486
- status: "error",
487
- steps: [],
488
- currentId: null,
489
- issues
490
- };
491
- return {
492
- status: "running",
493
- steps: [],
494
- currentId: flow.nodes.find((n) => n.type === "gatilho").id,
495
- issues: []
496
- };
497
- }
498
- /**
499
- * Executa o bloco ativo (um passo) e passa ao próximo. Na condição é preciso escolher o ramo:
500
- * sem escolha válida nada anda. A espera é instantânea; só o texto diz quanto aguardaria.
501
- */
502
- function advanceSimulation(flow, state, choice) {
503
- if (state.status !== "running" && state.status !== "waiting-choice" || !state.currentId) return state;
504
- const node = flow.nodes.find((n) => n.id === state.currentId);
505
- const edges = node ? flow.edges.filter((e) => e.source === node.id) : [];
506
- const fail = () => ({
507
- ...state,
508
- status: "error",
509
- currentId: null,
510
- issues: [{
511
- code: "fluxo-mudou",
512
- message: "O fluxo mudou durante o teste. Reinicie para testar de novo."
513
- }]
514
- });
515
- if (!node) return fail();
516
- let detail;
517
- let target;
518
- if (node.type === "condicao") {
519
- if (choice !== "sim" && choice !== "nao") return state;
520
- target = edges.find((e) => e.branch === choice)?.target;
521
- detail = `Escolha: ${BRANCH_NAME[choice]}`;
522
- if (!target) return fail();
523
- } else if (node.type !== "fim") {
524
- target = edges[0]?.target;
525
- if (node.type === "espera") detail = describeWait(node.data.config);
526
- if (!target) return fail();
527
- }
528
- const step = {
529
- nodeId: node.id,
530
- type: node.type,
531
- label: nodeName(node),
532
- status: "succeeded",
533
- ...detail ? { detail } : {}
534
- };
535
- const steps = [...state.steps, step];
536
- if (!target) return {
537
- status: "finished",
538
- steps,
539
- currentId: null,
540
- issues: []
541
- };
542
- const next = flow.nodes.find((n) => n.id === target);
543
- if (!next) return fail();
544
- return {
545
- status: next.type === "condicao" ? "waiting-choice" : "running",
546
- steps,
547
- currentId: target,
548
- issues: []
549
- };
550
- }
551
- //#endregion
552
- //#region src/components/ui/flow-viewer.tsx
553
- var FLOW_ICON = {
554
- gatilho: Zap,
555
- acao: Play,
556
- condicao: GitBranch,
557
- espera: Clock,
558
- fim: Flag
559
- };
560
- var TONE = {
561
- gatilho: "bg-primary-soft text-primary-soft-foreground",
562
- acao: "bg-info-soft text-info-soft-foreground",
563
- condicao: "bg-warning-soft text-warning-soft-foreground",
564
- espera: "bg-muted text-muted-foreground",
565
- fim: "bg-success-soft text-success-soft-foreground"
566
- };
567
- var DIRECTION = {
568
- up: "para cima",
569
- down: "para baixo",
570
- left: "para a esquerda",
571
- right: "para a direita"
572
- };
573
- /** Os textos que a biblioteca escreve no DOM (leitor de tela), em português. */
574
- var FLOW_ARIA = {
575
- "node.a11yDescription.default": "Pressione Enter para abrir o bloco, Espaço para selecioná-lo e Delete para apagá-lo. Esc cancela.",
576
- "node.a11yDescription.keyboardDisabled": "Pressione Enter para abrir o bloco, Espaço para selecioná-lo e Delete para apagá-lo. Esc cancela.",
577
- "node.a11yDescription.ariaLiveMessage": ({ direction, x, y }) => `Bloco movido ${DIRECTION[direction] ?? direction}. Nova posição, x: ${x}, y: ${y}`,
578
- "edge.a11yDescription.default": "Pressione Espaço para selecionar a ligação e Delete para apagá-la. Esc cancela."
579
- };
580
- /** Os blocos do Rendra como nós do React Flow (a biblioteca só vê isto). */
581
- function toRfNodes(flow, opts) {
582
- const byNode = /* @__PURE__ */ new Map();
583
- for (const issue of opts.issues ?? []) if (issue.nodeId) byNode.set(issue.nodeId, [...byNode.get(issue.nodeId) ?? [], issue.message]);
584
- return flow.nodes.map((n) => ({
585
- id: n.id,
586
- type: "bloco",
587
- position: n.position,
588
- ariaLabel: n.data.label,
589
- domAttributes: { "aria-roledescription": "bloco" },
590
- data: {
591
- label: n.data.label,
592
- kind: n.type,
593
- ...n.data.config ? { config: n.data.config } : {},
594
- issues: byNode.get(n.id) ?? [],
595
- active: opts.activeId === n.id,
596
- handles: opts.handles,
597
- edit: opts.edit
598
- }
599
- }));
600
- }
601
- /** As ligações do Rendra como arestas do React Flow, com a descrição em português. */
602
- function toRfEdges(flow) {
603
- const labelOf = new Map(flow.nodes.map((n) => [n.id, nodeName(n)]));
604
- return flow.edges.map((e) => ({
605
- id: e.id,
606
- source: e.source,
607
- target: e.target,
608
- ...e.branch ? { sourceHandle: e.branch } : {},
609
- ariaLabel: `De "${labelOf.get(e.source) ?? e.source}" para "${labelOf.get(e.target) ?? e.target}"${e.branch ? `, se ${e.branch === "sim" ? "sim" : "não"}` : ""}`,
610
- domAttributes: { "aria-roledescription": "ligação" }
611
- }));
1101
+ function Value({ text }) {
1102
+ if (!text) return /* @__PURE__ */ jsx("span", {
1103
+ className: "text-xs text-muted-foreground",
1104
+ children: "Não definido"
1105
+ });
1106
+ return /* @__PURE__ */ jsx("span", {
1107
+ className: "flex flex-wrap items-center gap-1 text-xs break-words",
1108
+ children: splitVariables(text).map((part, i) => part.kind === "var" ? /* @__PURE__ */ jsx("span", {
1109
+ "data-flow-chip": true,
1110
+ className: "rounded-item bg-primary-soft px-2 py-1 text-primary-soft-foreground",
1111
+ children: `{{${part.value}}}`
1112
+ }, i) : /* @__PURE__ */ jsx("span", { children: part.value }, i))
1113
+ });
612
1114
  }
613
1115
  function FlowBlock({ id, data, selected, isConnectable }) {
614
1116
  const lib = useMotionLib();
615
1117
  const presets = useMotionPresets();
1118
+ const actions = useContext(FlowActionsContext);
616
1119
  const uid = useId();
617
1120
  const Icon = FLOW_ICON[data.kind];
618
- const wait = data.kind === "espera" ? formatWait(data.config) : null;
1121
+ const def = FLOW_BLOCKS[data.kind];
1122
+ const shape = {
1123
+ type: data.kind,
1124
+ data: {
1125
+ label: data.label,
1126
+ config: data.config
1127
+ }
1128
+ };
1129
+ const name = nodeName(shape);
1130
+ const summary = summarizeConfig(shape);
1131
+ const rules = data.kind === "condicao" ? rulesOf(shape) : [];
1132
+ const received = data.kind === "webhook-entrada" && data.config?.received === true;
619
1133
  const invalid = data.issues.length > 0;
620
1134
  const issueId = `${uid}-problema`;
621
- const handle = cn("rounded-full border-2 border-card", data.handles === "lg" ? "size-6" : "size-2");
1135
+ const descId = `${uid}-descricao`;
622
1136
  const ring = "pointer-events-none absolute -inset-1 rounded-block border-2 border-primary";
1137
+ const action = "nodrag nopan min-h-touch min-w-touch";
623
1138
  return /* @__PURE__ */ jsxs("div", {
624
1139
  "data-flow-node": true,
625
1140
  "data-kind": data.kind,
@@ -627,8 +1142,8 @@ function FlowBlock({ id, data, selected, isConnectable }) {
627
1142
  "data-active": data.active ? "true" : void 0,
628
1143
  "data-invalid": invalid ? "true" : void 0,
629
1144
  "data-selected": selected ? "true" : void 0,
630
- "aria-describedby": invalid ? issueId : void 0,
631
- className: cn("relative w-flow-node rounded-block border bg-card p-3 text-card-foreground shadow-sm", data.kind === "condicao" && "pr-12 pb-8", invalid && "border-destructive", data.active && "border-primary", selected && "ring-2 ring-primary"),
1145
+ "aria-describedby": invalid ? `${descId} ${issueId}` : descId,
1146
+ className: cn("relative w-flow-node rounded-block border bg-card text-card-foreground shadow-sm", invalid && "border-destructive", data.active && "border-primary", selected && "ring-2 ring-primary"),
632
1147
  children: [
633
1148
  data.active && (lib ? /* @__PURE__ */ jsx(lib.m.span, {
634
1149
  "aria-hidden": true,
@@ -648,17 +1163,15 @@ function FlowBlock({ id, data, selected, isConnectable }) {
648
1163
  "aria-hidden": true,
649
1164
  className: ring
650
1165
  })),
651
- data.kind !== "gatilho" && /* @__PURE__ */ jsx(Handle, {
1166
+ !(def.group === "gatilhos") && /* @__PURE__ */ jsx(Port, {
652
1167
  type: "target",
653
- position: Position.Top,
654
- className: handle,
655
1168
  isConnectable
656
1169
  }),
657
1170
  /* @__PURE__ */ jsxs("div", {
658
- className: "flex items-start gap-3",
1171
+ className: "flex items-start gap-3 p-3 pb-2",
659
1172
  children: [
660
1173
  /* @__PURE__ */ jsx("span", {
661
- className: cn("flex size-8 shrink-0 items-center justify-center rounded-block", TONE[data.kind]),
1174
+ className: cn("flex size-8 shrink-0 items-center justify-center rounded-block", flowTone(data.kind)),
662
1175
  children: /* @__PURE__ */ jsx(Icon, {
663
1176
  className: "size-icon-md",
664
1177
  "aria-hidden": true
@@ -666,42 +1179,117 @@ function FlowBlock({ id, data, selected, isConnectable }) {
666
1179
  }),
667
1180
  /* @__PURE__ */ jsxs("div", {
668
1181
  className: "flex min-w-0 flex-1 flex-col",
1182
+ children: [/* @__PURE__ */ jsx("span", {
1183
+ "data-flow-title": true,
1184
+ className: "truncate text-sm font-medium",
1185
+ title: name,
1186
+ children: name
1187
+ }), /* @__PURE__ */ jsx("span", {
1188
+ className: "truncate text-xs text-muted-foreground",
1189
+ children: data.active ? "Ativo" : FLOW_TYPE_NAME[data.kind]
1190
+ })]
1191
+ }),
1192
+ actions && /* @__PURE__ */ jsxs("div", {
1193
+ className: "-mt-3 -mr-3 flex shrink-0",
669
1194
  children: [
670
- /* @__PURE__ */ jsx("span", {
671
- className: "text-xs text-muted-foreground",
672
- children: FLOW_TYPE_NAME[data.kind]
673
- }),
674
- /* @__PURE__ */ jsx("span", {
675
- className: "text-sm font-medium break-words",
676
- children: data.label
1195
+ /* @__PURE__ */ jsx(Button, {
1196
+ variant: "ghost",
1197
+ size: "sm",
1198
+ iconOnly: true,
1199
+ "aria-label": `Executar bloco ${name}`,
1200
+ className: action,
1201
+ icon: /* @__PURE__ */ jsx(Play, {}),
1202
+ onClick: () => actions.play(id)
677
1203
  }),
678
- wait && /* @__PURE__ */ jsx("span", {
679
- className: "text-xs text-muted-foreground",
680
- children: wait
1204
+ actions.edit && /* @__PURE__ */ jsx(Button, {
1205
+ "data-flow-edit": true,
1206
+ variant: "ghost",
1207
+ size: "sm",
1208
+ iconOnly: true,
1209
+ "aria-label": `Editar bloco ${name}`,
1210
+ className: action,
1211
+ icon: /* @__PURE__ */ jsx(SquarePen, {}),
1212
+ onClick: () => actions.edit?.(id)
681
1213
  }),
682
- data.active && /* @__PURE__ */ jsx("span", {
683
- className: "text-xs font-medium text-primary-text",
684
- children: "Ativo"
1214
+ actions.remove && /* @__PURE__ */ jsx(Button, {
1215
+ variant: "ghost",
1216
+ size: "sm",
1217
+ iconOnly: true,
1218
+ "aria-label": `Excluir bloco ${name}`,
1219
+ className: action,
1220
+ icon: /* @__PURE__ */ jsx(Trash2, {}),
1221
+ onClick: () => actions.remove?.(id)
685
1222
  })
686
1223
  ]
687
- }),
688
- data.edit && /* @__PURE__ */ jsx(Button, {
689
- "data-flow-edit": true,
690
- variant: "ghost",
691
- size: "sm",
692
- iconOnly: true,
693
- "aria-label": `Editar bloco ${nodeName({
694
- type: data.kind,
695
- data
696
- })}`,
697
- className: "nodrag nopan -mt-1 -mr-1 min-h-touch min-w-touch shrink-0",
698
- icon: /* @__PURE__ */ jsx(SquarePen, {})
699
1224
  })
700
1225
  ]
701
1226
  }),
1227
+ /* @__PURE__ */ jsx("p", {
1228
+ id: descId,
1229
+ className: "px-3 pb-3 text-xs text-muted-foreground",
1230
+ children: def.description
1231
+ }),
1232
+ summary.length > 0 && /* @__PURE__ */ jsx("dl", {
1233
+ className: "flex flex-col gap-2 border-t p-3",
1234
+ children: summary.map((row) => /* @__PURE__ */ jsxs("div", {
1235
+ className: "flex flex-col gap-1",
1236
+ children: [/* @__PURE__ */ jsx("dt", {
1237
+ className: "text-xs text-muted-foreground",
1238
+ children: row.label
1239
+ }), /* @__PURE__ */ jsx("dd", { children: /* @__PURE__ */ jsx(Value, { text: row.value }) })]
1240
+ }, row.label))
1241
+ }),
1242
+ received && /* @__PURE__ */ jsxs("p", {
1243
+ className: "mx-3 mb-3 flex items-center gap-2 rounded-block bg-success-soft p-2 text-xs text-success-soft-foreground",
1244
+ children: [/* @__PURE__ */ jsx(CheckCircle2, {
1245
+ className: "size-icon-sm shrink-0",
1246
+ "aria-hidden": true
1247
+ }), "Dados de exemplo recebidos"]
1248
+ }),
1249
+ data.kind === "condicao" && /* @__PURE__ */ jsxs("div", {
1250
+ className: "flex flex-col border-t",
1251
+ children: [rules.map((rule) => /* @__PURE__ */ jsxs("div", {
1252
+ "data-flow-output": rule.id,
1253
+ className: "relative flex min-h-touch items-center justify-between gap-2 border-b px-3 pr-4",
1254
+ children: [
1255
+ /* @__PURE__ */ jsx("span", {
1256
+ className: "text-xs font-medium",
1257
+ children: branchName(shape, rule.id)
1258
+ }),
1259
+ /* @__PURE__ */ jsx("span", {
1260
+ className: "min-w-0 truncate text-xs text-muted-foreground",
1261
+ title: ruleSummary(rule),
1262
+ children: ruleSummary(rule)
1263
+ }),
1264
+ /* @__PURE__ */ jsx(Port, {
1265
+ type: "source",
1266
+ id: rule.id,
1267
+ isConnectable
1268
+ })
1269
+ ]
1270
+ }, rule.id)), /* @__PURE__ */ jsxs("div", {
1271
+ "data-flow-output": "senao",
1272
+ className: "relative flex min-h-touch items-center justify-between gap-2 px-3 pr-4",
1273
+ children: [
1274
+ /* @__PURE__ */ jsx("span", {
1275
+ className: "text-xs font-medium",
1276
+ children: "Senão"
1277
+ }),
1278
+ /* @__PURE__ */ jsx("span", {
1279
+ className: "min-w-0 truncate text-xs text-muted-foreground",
1280
+ children: "Nenhuma regra bateu"
1281
+ }),
1282
+ /* @__PURE__ */ jsx(Port, {
1283
+ type: "source",
1284
+ id: "senao",
1285
+ isConnectable
1286
+ })
1287
+ ]
1288
+ })]
1289
+ }),
702
1290
  invalid && /* @__PURE__ */ jsxs("p", {
703
1291
  id: issueId,
704
- className: "mt-2 flex flex-wrap gap-x-1 rounded-block bg-destructive-soft p-2 text-xs text-destructive-soft-foreground",
1292
+ className: "m-3 mt-0 flex flex-wrap gap-x-1 rounded-block bg-destructive-soft p-2 text-xs text-destructive-soft-foreground",
705
1293
  children: [
706
1294
  /* @__PURE__ */ jsx(TriangleAlert, {
707
1295
  className: "size-icon-sm shrink-0",
@@ -712,42 +1300,119 @@ function FlowBlock({ id, data, selected, isConnectable }) {
712
1300
  data.issues.join(" ")
713
1301
  ]
714
1302
  }),
715
- data.kind === "condicao" && /* @__PURE__ */ jsxs(Fragment$1, { children: [
1303
+ data.kind !== "condicao" && /* @__PURE__ */ jsx(Port, {
1304
+ type: "source",
1305
+ isConnectable
1306
+ })
1307
+ ]
1308
+ });
1309
+ }
1310
+ /** Ponto da curva de Bézier de uma ligação (t entre 0 e 1), com os mesmos controles da biblioteca. */
1311
+ function bezierAt(sx, sy, tx, ty, t) {
1312
+ const d = tx - sx;
1313
+ const offset = d >= 0 ? .5 * d : 6.25 * Math.sqrt(-d);
1314
+ const mt = 1 - t;
1315
+ return {
1316
+ x: mt ** 3 * sx + 3 * mt ** 2 * t * (sx + offset) + 3 * mt * t ** 2 * (tx - offset) + t ** 3 * tx,
1317
+ y: mt ** 3 * sy + 3 * mt ** 2 * t * sy + 3 * mt * t ** 2 * ty + t ** 3 * ty
1318
+ };
1319
+ }
1320
+ function FlowLink({ id, sourceX, sourceY, targetX, targetY, sourcePosition, targetPosition, markerEnd, selected, data }) {
1321
+ const actions = useContext(FlowActionsContext);
1322
+ const [confirming, setConfirming] = useState(false);
1323
+ const noRef = useRef(null);
1324
+ useEffect(() => {
1325
+ if (confirming) noRef.current?.focus();
1326
+ }, [confirming]);
1327
+ const [path, midX, midY] = getBezierPath({
1328
+ sourceX,
1329
+ sourceY,
1330
+ sourcePosition,
1331
+ targetX,
1332
+ targetY,
1333
+ targetPosition
1334
+ });
1335
+ const start = bezierAt(sourceX, sourceY, targetX, targetY, .18);
1336
+ const showTrash = Boolean(selected && actions?.removeEdge);
1337
+ const names = `${data?.from ?? ""} para ${data?.to ?? ""}`;
1338
+ const label = data?.label;
1339
+ return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(BaseEdge, {
1340
+ id,
1341
+ path,
1342
+ markerEnd,
1343
+ interactionWidth: 24
1344
+ }), /* @__PURE__ */ jsxs(EdgeLabelRenderer, { children: [label && /* @__PURE__ */ jsx("div", {
1345
+ className: "pointer-events-none absolute top-0 left-0 translate-x-(--flow-x) translate-y-(--flow-y)",
1346
+ style: {
1347
+ "--flow-x": `${start.x}px`,
1348
+ "--flow-y": `${start.y}px`
1349
+ },
1350
+ children: /* @__PURE__ */ jsx("span", {
1351
+ "data-flow-edge-label": true,
1352
+ className: "block -translate-x-1/2 -translate-y-1/2 rounded-item border bg-card px-2 py-1 text-xs font-medium text-card-foreground",
1353
+ children: label
1354
+ })
1355
+ }), showTrash && actions?.removeEdge && /* @__PURE__ */ jsx("div", {
1356
+ className: "pointer-events-none absolute top-0 left-0 z-50 translate-x-(--flow-x) translate-y-(--flow-y)",
1357
+ style: {
1358
+ "--flow-x": `${midX}px`,
1359
+ "--flow-y": `${midY}px`
1360
+ },
1361
+ children: /* @__PURE__ */ jsx("div", {
1362
+ "data-flow-edge-actions": true,
1363
+ role: "presentation",
1364
+ className: "nodrag nopan pointer-events-auto flex -translate-x-1/2 -translate-y-1/2 items-center gap-2 rounded-control border bg-card p-1 shadow-md",
1365
+ onKeyDown: (e) => {
1366
+ if (e.key === "Escape" && confirming) {
1367
+ e.stopPropagation();
1368
+ setConfirming(false);
1369
+ }
1370
+ },
1371
+ children: confirming ? /* @__PURE__ */ jsxs(Fragment$1, { children: [
716
1372
  /* @__PURE__ */ jsx("span", {
717
- className: "absolute inset-y-0 right-4 flex items-center text-xs font-medium",
1373
+ role: "alertdialog",
1374
+ "aria-label": "Confirmar exclusão da ligação",
1375
+ className: "px-2 text-xs font-medium",
1376
+ children: "Excluir ligação?"
1377
+ }),
1378
+ /* @__PURE__ */ jsx(Button, {
1379
+ size: "sm",
1380
+ variant: "destructive",
1381
+ className: "min-h-touch",
1382
+ onClick: () => {
1383
+ setConfirming(false);
1384
+ actions.removeEdge?.(id);
1385
+ },
718
1386
  children: "Sim"
719
1387
  }),
720
- /* @__PURE__ */ jsx("span", {
721
- className: "absolute inset-x-0 bottom-3 text-center text-xs font-medium",
1388
+ /* @__PURE__ */ jsx(Button, {
1389
+ size: "sm",
1390
+ variant: "outline",
1391
+ ref: noRef,
1392
+ className: "min-h-touch",
1393
+ onClick: (e) => {
1394
+ setConfirming(false);
1395
+ e.currentTarget.closest(".flow-surface")?.focus();
1396
+ },
722
1397
  children: "Não"
723
- }),
724
- /* @__PURE__ */ jsx(Handle, {
725
- id: "sim",
726
- type: "source",
727
- position: Position.Right,
728
- className: handle,
729
- isConnectable
730
- }),
731
- /* @__PURE__ */ jsx(Handle, {
732
- id: "nao",
733
- type: "source",
734
- position: Position.Bottom,
735
- className: handle,
736
- isConnectable
737
1398
  })
738
- ] }),
739
- data.kind !== "condicao" && data.kind !== "fim" && /* @__PURE__ */ jsx(Handle, {
740
- type: "source",
741
- position: Position.Bottom,
742
- className: handle,
743
- isConnectable
1399
+ ] }) : /* @__PURE__ */ jsx(Button, {
1400
+ size: "sm",
1401
+ variant: "ghost",
1402
+ iconOnly: true,
1403
+ "aria-label": `Excluir ligação de ${names}`,
1404
+ className: "min-h-touch min-w-touch",
1405
+ icon: /* @__PURE__ */ jsx(Trash2, {}),
1406
+ onClick: () => setConfirming(true)
744
1407
  })
745
- ]
746
- });
1408
+ })
1409
+ })] })] });
747
1410
  }
748
1411
  var nodeTypes = { bloco: FlowBlock };
1412
+ var edgeTypes = { ligacao: FlowLink };
749
1413
  var defaultEdgeOptions = {
750
- type: "smoothstep",
1414
+ type: "ligacao",
1415
+ animated: true,
751
1416
  markerEnd: { type: MarkerType.ArrowClosed }
752
1417
  };
753
1418
  var fitViewOptions = {
@@ -796,47 +1461,78 @@ function FlowZoomControls() {
796
1461
  ]
797
1462
  });
798
1463
  }
799
- /** Enquadra de novo quando a largura do canvas muda (painel do teste abrindo, janela redimensionada). */
1464
+ /** Zoom mínimo do enquadramento inicial: abaixo disso os cards e as alças ficam ilegíveis e pequenos demais. */
1465
+ var MIN_FIT_ZOOM = .5;
1466
+ /**
1467
+ * Enquadramento inicial e quando a largura do canvas muda. Cabe tudo se o zoom ficar em 50 % ou
1468
+ * mais; senão abre em 50 % começando pela esquerda (pelo gatilho) e a pessoa navega. O botão
1469
+ * "Enquadrar" continua mostrando o fluxo inteiro, sem piso de zoom.
1470
+ */
800
1471
  function FlowAutoFit() {
801
- const { fitView } = useReactFlow();
1472
+ const { getNodes, setViewport } = useReactFlow();
802
1473
  const width = useStore((st) => st.width);
1474
+ const height = useStore((st) => st.height);
1475
+ const ready = useStore((st) => st.nodeLookup.size > 0 && [...st.nodeLookup.values()].every((n) => (n.measured.width ?? 0) > 0));
803
1476
  const before = useRef(0);
1477
+ const [attempt, setAttempt] = useState(0);
804
1478
  useEffect(() => {
805
- if (before.current > 0 && width > 0 && width !== before.current) fitView(fitViewOptions);
806
- if (width > 0) before.current = width;
807
- }, [width, fitView]);
1479
+ if (!ready || width <= 0 || height <= 0 || width === before.current) return;
1480
+ const nodes = getNodes();
1481
+ if (nodes.length === 0) return;
1482
+ const b = getNodesBounds(nodes);
1483
+ const fit = Math.min(1, (width - 32) / b.width, (height - 128) / b.height);
1484
+ const zoom = Math.max(fit, MIN_FIT_ZOOM);
1485
+ const x = zoom === fit ? (width - b.width * zoom) / 2 - b.x * zoom : 16 - b.x * zoom;
1486
+ const y = Math.max((height - b.height * zoom) / 2, 64) - b.y * zoom;
1487
+ setViewport({
1488
+ x,
1489
+ y,
1490
+ zoom
1491
+ }).then((ok) => {
1492
+ if (ok) before.current = width;
1493
+ else if (attempt < 20) setTimeout(() => setAttempt((a) => a + 1), 50);
1494
+ });
1495
+ }, [
1496
+ ready,
1497
+ width,
1498
+ height,
1499
+ attempt,
1500
+ getNodes,
1501
+ setViewport
1502
+ ]);
808
1503
  return null;
809
1504
  }
810
1505
  /**
811
1506
  * O canvas: contêiner com a altura que sobra na tela (`h-board`), tema pelos tokens e a
812
1507
  * biblioteca por dentro. A rolagem horizontal é do canvas (`data-allow-overflow`), nunca da página.
813
1508
  */
814
- function FlowSurface({ className, children, ...props }) {
1509
+ function FlowSurface({ className, children, fill = true, ...props }) {
815
1510
  const ref = useFillHeight();
816
1511
  return /* @__PURE__ */ jsx("div", {
817
- ref,
1512
+ ref: fill ? ref : void 0,
1513
+ tabIndex: -1,
818
1514
  "data-allow-overflow": true,
819
- className: cn("flow-surface relative h-board min-h-viewer w-full min-w-0 overflow-hidden rounded-surface border", className),
1515
+ className: cn("flow-surface relative w-full min-w-0 overflow-hidden outline-none", fill ? "h-board min-h-viewer rounded-surface border" : "h-full", className),
820
1516
  style: {
821
1517
  "--xy-edge-stroke": "var(--rendra-muted-foreground)",
822
1518
  "--xy-edge-stroke-selected": "var(--rendra-primary)",
823
1519
  "--xy-edge-stroke-width": "2",
824
1520
  "--xy-connectionline-stroke": "var(--rendra-primary)",
825
1521
  "--xy-connectionline-stroke-width": "2",
826
- "--xy-handle-background-color": "var(--rendra-primary)",
1522
+ "--xy-handle-background-color": "var(--rendra-primary-text)",
827
1523
  "--xy-attribution-background-color": "var(--rendra-card)",
828
1524
  "--xy-background-color": "var(--rendra-muted)"
829
1525
  },
830
1526
  children: /* @__PURE__ */ jsxs(ReactFlow, {
831
1527
  className: "flow-canvas",
832
1528
  nodeTypes,
1529
+ edgeTypes,
833
1530
  defaultEdgeOptions,
834
1531
  defaultMarkerColor: "var(--rendra-muted-foreground)",
835
- connectionLineType: ConnectionLineType.SmoothStep,
1532
+ connectionLineType: ConnectionLineType.Bezier,
1533
+ connectionRadius: 12,
836
1534
  ariaLabelConfig: FLOW_ARIA,
837
- fitView: true,
838
- fitViewOptions,
839
- minZoom: .25,
1535
+ minZoom: .1,
840
1536
  maxZoom: 2,
841
1537
  zoomOnDoubleClick: false,
842
1538
  zoomOnScroll: false,
@@ -855,17 +1551,16 @@ function FlowSurface({ className, children, ...props }) {
855
1551
  * não apaga. Para montar e editar, use o FlowEditor.
856
1552
  */
857
1553
  function FlowViewer({ value, activeId = null, issues, className }) {
858
- const nodes = useMemo(() => toRfNodes(value, {
1554
+ const flow = useMemo(() => migrateFlow(value), [value]);
1555
+ const nodes = useMemo(() => toRfNodes(flow, {
859
1556
  activeId,
860
- issues,
861
- handles: "sm",
862
- edit: false
1557
+ issues
863
1558
  }), [
864
- value,
1559
+ flow,
865
1560
  activeId,
866
1561
  issues
867
1562
  ]);
868
- const edges = useMemo(() => toRfEdges(value), [value]);
1563
+ const edges = useMemo(() => toRfEdges(flow), [flow]);
869
1564
  return /* @__PURE__ */ jsx("div", {
870
1565
  "data-rendra": "FLX-002",
871
1566
  className: cn("flow-viewer min-w-0", className),
@@ -881,66 +1576,818 @@ function FlowViewer({ value, activeId = null, issues, className }) {
881
1576
  });
882
1577
  }
883
1578
  //#endregion
884
- //#region src/components/ui/flow-editor.tsx
885
- var NODE_W = 256;
886
- var NODE_H = 72;
887
- var DRAG_START = 4;
888
- var NONE = "__nenhum";
889
- /** Reaproveita os objetos de nó que não mudaram (a biblioteca guarda medida e seleção neles). */
890
- function mergeNodes(prev, next) {
891
- const old = new Map(prev.map((n) => [n.id, n]));
892
- return next.map((n) => {
893
- const o = old.get(n.id);
894
- if (!o) return n;
895
- const a = o.data;
896
- const b = n.data;
897
- if (!o.dragging && o.position.x === n.position.x && o.position.y === n.position.y && o.ariaLabel === n.ariaLabel && a.label === b.label && a.kind === b.kind && a.config === b.config && a.active === b.active && a.handles === b.handles && a.edit === b.edit && a.issues.length === b.issues.length && a.issues.every((m, i) => m === b.issues[i])) return o;
898
- return {
899
- ...o,
900
- position: o.dragging ? o.position : n.position,
901
- ariaLabel: n.ariaLabel,
902
- data: b
903
- };
904
- });
905
- }
906
- function mergeEdges(prev, next) {
907
- const old = new Map(prev.map((e) => [e.id, e]));
908
- return next.map((e) => {
909
- const o = old.get(e.id);
910
- if (!o) return e;
911
- return o.source === e.source && o.target === e.target && o.sourceHandle === e.sourceHandle && o.ariaLabel === e.ariaLabel ? o : {
912
- ...o,
913
- ...e
914
- };
1579
+ //#region src/components/ui/flow-sidebar.tsx
1580
+ var normalize = (s) => s.normalize("NFD").replace(/\p{M}/gu, "").toLowerCase();
1581
+ function FlowBlockList({ onAdd, onDragStart, suppressClick }) {
1582
+ const [query, setQuery] = useState("");
1583
+ const q = normalize(query.trim());
1584
+ const groups = FLOW_BLOCK_GROUPS.map((g) => ({
1585
+ ...g,
1586
+ types: g.types.filter((t) => {
1587
+ const def = FLOW_BLOCKS[t];
1588
+ return !def.hidden && (q === "" || normalize(`${def.name} ${def.hint}`).includes(q));
1589
+ })
1590
+ })).filter((g) => g.types.length > 0);
1591
+ return /* @__PURE__ */ jsxs("div", {
1592
+ className: "flex flex-col gap-4",
1593
+ children: [
1594
+ /* @__PURE__ */ jsx(Input, {
1595
+ icon: /* @__PURE__ */ jsx(Search, {}),
1596
+ value: query,
1597
+ onChange: setQuery,
1598
+ placeholder: "Buscar gatilho ou ação",
1599
+ "aria-label": "Buscar bloco",
1600
+ clearable: true
1601
+ }),
1602
+ groups.length === 0 && /* @__PURE__ */ jsx(EmptyState, {
1603
+ size: "compact",
1604
+ title: "Nenhum bloco encontrado",
1605
+ description: "Nenhum bloco tem esse nome."
1606
+ }),
1607
+ groups.map((g) => /* @__PURE__ */ jsxs("section", {
1608
+ role: "group",
1609
+ "aria-label": g.name,
1610
+ className: "flex flex-col gap-2",
1611
+ children: [/* @__PURE__ */ jsx("h3", {
1612
+ className: "text-xs font-medium text-muted-foreground",
1613
+ children: g.name
1614
+ }), g.types.map((type) => {
1615
+ const Icon = FLOW_ICON[type];
1616
+ return /* @__PURE__ */ jsx(Button, {
1617
+ variant: "outline",
1618
+ size: "sm",
1619
+ "data-flow-block": type,
1620
+ className: cn("w-full justify-start", onDragStart && "cursor-grab touch-none"),
1621
+ icon: /* @__PURE__ */ jsx("span", {
1622
+ className: cn("flex size-6 items-center justify-center rounded-block", flowTone(type)),
1623
+ children: /* @__PURE__ */ jsx(Icon, {
1624
+ className: "size-icon-sm",
1625
+ "aria-hidden": true
1626
+ })
1627
+ }),
1628
+ onPointerDown: onDragStart ? (e) => onDragStart(e, type) : void 0,
1629
+ onClick: () => {
1630
+ if (suppressClick?.current) return;
1631
+ onAdd(type);
1632
+ },
1633
+ children: FLOW_BLOCKS[type].name
1634
+ }, type);
1635
+ })]
1636
+ }, g.id))
1637
+ ]
1638
+ });
1639
+ }
1640
+ /** Cabeçalho comum dos painéis da barra lateral: título e o botão de fechar. */
1641
+ function FlowPanelShell({ title, onClose, closeLabel = "Fechar painel", children, className }) {
1642
+ const id = useId();
1643
+ return /* @__PURE__ */ jsxs("section", {
1644
+ "aria-labelledby": id,
1645
+ className: cn("flex flex-col gap-4", className),
1646
+ children: [/* @__PURE__ */ jsxs("div", {
1647
+ className: "flex items-center justify-between gap-2",
1648
+ children: [/* @__PURE__ */ jsx("h2", {
1649
+ id,
1650
+ className: "text-sm font-semibold",
1651
+ children: title
1652
+ }), onClose && /* @__PURE__ */ jsx(Button, {
1653
+ variant: "ghost",
1654
+ size: "sm",
1655
+ iconOnly: true,
1656
+ "aria-label": closeLabel,
1657
+ className: "min-h-touch min-w-touch",
1658
+ icon: /* @__PURE__ */ jsx(X, {}),
1659
+ onClick: onClose
1660
+ })]
1661
+ }), children]
915
1662
  });
916
1663
  }
917
- function draftOf(node, flow) {
918
- const target = (branch) => flow.edges.find((e) => e.source === node.id && (branch ? e.branch === branch : true))?.target ?? NONE;
919
- const cfg = node.data.config;
920
- const unit = FLOW_WAIT_UNITS.find((u) => u === cfg?.unit) ?? "minutos";
1664
+ //#endregion
1665
+ //#region src/lib/flow-edit.ts
1666
+ /** Acrescenta um bloco com id único e determinístico (`tipo-N`) e a configuração padrão do tipo. */
1667
+ function addNode(input, type, position, options = {}) {
1668
+ const flow = migrateFlow(input);
1669
+ const taken = /* @__PURE__ */ new Set([...flow.nodes.map((n) => n.id), ...options.reservedIds ?? []]);
1670
+ let n = 1;
1671
+ while (taken.has(`${type}-${n}`)) n++;
1672
+ const id = `${type}-${n}`;
1673
+ const config = FLOW_BLOCKS[type].defaultConfig();
1674
+ const node = {
1675
+ id,
1676
+ type,
1677
+ position: {
1678
+ x: position.x,
1679
+ y: position.y
1680
+ },
1681
+ data: Object.keys(config).length > 0 ? {
1682
+ label: options.label ?? FLOW_TYPE_NAME[type],
1683
+ config
1684
+ } : { label: options.label ?? FLOW_TYPE_NAME[type] }
1685
+ };
921
1686
  return {
922
- label: node.data.label,
923
- duration: typeof cfg?.duration === "number" ? String(cfg.duration) : "",
924
- unit,
925
- next: node.type === "condicao" ? NONE : target(),
926
- sim: node.type === "condicao" ? target("sim") : NONE,
927
- nao: node.type === "condicao" ? target("nao") : NONE
1687
+ flow: {
1688
+ ...flow,
1689
+ nodes: [...flow.nodes, node]
1690
+ },
1691
+ id
928
1692
  };
929
1693
  }
930
- var sameDraft = (a, b) => a.label === b.label && a.duration === b.duration && a.unit === b.unit && a.next === b.next && a.sim === b.sim && a.nao === b.nao;
931
- function BlockDrawer({ flow, nodeId, open, onOpenChange, onApply, onDelete }) {
932
- const node = flow.nodes.find((n) => n.id === nodeId);
933
- const initial = useMemo(() => node ? draftOf(node, flow) : null, [node, flow]);
934
- const [draft, setDraft] = useState(initial);
935
- const [discarding, setDiscarding] = useState(false);
936
- if (!node || !initial || !draft) return null;
937
- const dirty = !sameDraft(draft, initial);
938
- const duration = Number(draft.duration);
939
- const durationError = node.type === "espera" && (draft.duration.trim() === "" || !(duration > 0)) ? "Informe um número maior que zero." : void 0;
940
- const set = (patch) => setDraft({
941
- ...draft,
942
- ...patch
1694
+ var edgeId = (source, target, branch) => branch ? `${source}:${branch}->${target}` : `${source}->${target}`;
1695
+ /** Blocos que `id` alcança e que nenhum outro caminho do gatilho alcança sem passar por ele. */
1696
+ function exclusiveDescendants(flow, id) {
1697
+ const reach = (starts, skip) => {
1698
+ const seen = /* @__PURE__ */ new Set();
1699
+ const stack = [...starts];
1700
+ while (stack.length) {
1701
+ const cur = stack.pop();
1702
+ if (seen.has(cur) || cur === skip) continue;
1703
+ seen.add(cur);
1704
+ for (const e of flow.edges) if (e.source === cur) stack.push(e.target);
1705
+ }
1706
+ return seen;
1707
+ };
1708
+ const below = reach([id]);
1709
+ below.delete(id);
1710
+ const alive = reach(flow.nodes.filter((n) => isTrigger(n.type) && n.id !== id).map((n) => n.id), id);
1711
+ return flow.nodes.filter((n) => below.has(n.id) && !alive.has(n.id)).map((n) => n.id);
1712
+ }
1713
+ /**
1714
+ * Apaga o bloco. `so-este`: religa o anterior ao posterior quando o bloco tem uma entrada e uma
1715
+ * saída e não é condição. `daqui-para-frente`: apaga também tudo o que só existe por causa dele.
1716
+ */
1717
+ function removeNode(input, id, mode = "so-este") {
1718
+ const flow = migrateFlow(input);
1719
+ const node = flow.nodes.find((n) => n.id === id);
1720
+ if (!node) return flow;
1721
+ const gone = /* @__PURE__ */ new Set([id, ...mode === "daqui-para-frente" ? exclusiveDescendants(flow, id) : []]);
1722
+ let edges = flow.edges.filter((e) => !gone.has(e.source) && !gone.has(e.target));
1723
+ if (mode === "so-este" && node.type !== "condicao") {
1724
+ const into = flow.edges.filter((e) => e.target === id);
1725
+ const from = flow.edges.filter((e) => e.source === id);
1726
+ if (into.length === 1 && from.length === 1) {
1727
+ const a = into[0];
1728
+ const b = from[0];
1729
+ const bridged = {
1730
+ id: edgeId(a.source, b.target, a.branch),
1731
+ source: a.source,
1732
+ target: b.target,
1733
+ ...a.branch ? { branch: a.branch } : {}
1734
+ };
1735
+ if (a.source !== b.target && !edges.some((e) => e.id === bridged.id)) edges = [...edges, bridged];
1736
+ }
1737
+ }
1738
+ return {
1739
+ ...flow,
1740
+ nodes: flow.nodes.filter((n) => !gone.has(n.id)),
1741
+ edges
1742
+ };
1743
+ }
1744
+ function removeEdge(flow, id) {
1745
+ return {
1746
+ ...flow,
1747
+ edges: flow.edges.filter((e) => e.id !== id)
1748
+ };
1749
+ }
1750
+ /** Tira a ligação da saída `branch` de `source` (a única saída, nos blocos comuns). */
1751
+ function clearOutput(flow, source, branch) {
1752
+ return {
1753
+ ...flow,
1754
+ edges: flow.edges.filter((e) => !(e.source === source && (e.branch ?? void 0) === branch))
1755
+ };
1756
+ }
1757
+ function moveNode(flow, id, position) {
1758
+ return {
1759
+ ...flow,
1760
+ nodes: flow.nodes.map((n) => n.id === id ? {
1761
+ ...n,
1762
+ position: {
1763
+ x: position.x,
1764
+ y: position.y
1765
+ }
1766
+ } : n)
1767
+ };
1768
+ }
1769
+ /** Troca o rótulo e/ou o config do bloco (o config informado substitui o anterior). */
1770
+ function updateNodeData(flow, id, patch) {
1771
+ return {
1772
+ ...flow,
1773
+ nodes: flow.nodes.map((n) => {
1774
+ if (n.id !== id) return n;
1775
+ const label = patch.label ?? n.data.label;
1776
+ const config = patch.config ?? n.data.config;
1777
+ return {
1778
+ ...n,
1779
+ data: config ? {
1780
+ label,
1781
+ config
1782
+ } : { label }
1783
+ };
1784
+ })
1785
+ };
1786
+ }
1787
+ /**
1788
+ * Liga dois blocos. A segunda ligação numa saída substitui a primeira e devolve a trocada em
1789
+ * `replaced`, para o editor avisar. Recusa, com o motivo, o que o fluxo nunca aceita.
1790
+ */
1791
+ function connectNodes(flow, source, target, branch) {
1792
+ const from = flow.nodes.find((n) => n.id === source);
1793
+ const to = flow.nodes.find((n) => n.id === target);
1794
+ if (!from || !to) return {
1795
+ ok: false,
1796
+ reason: "Esse bloco não existe mais."
1797
+ };
1798
+ if (source === target) return {
1799
+ ok: false,
1800
+ reason: "Um bloco não pode ligar a ele mesmo."
1801
+ };
1802
+ if (isTrigger(to.type)) return {
1803
+ ok: false,
1804
+ reason: "O gatilho começa o fluxo e não recebe ligação."
1805
+ };
1806
+ if (from.type === "condicao") {
1807
+ if (!branch) return {
1808
+ ok: false,
1809
+ reason: "Escolha a saída da condição: uma regra ou \"Senão\"."
1810
+ };
1811
+ if (!outputIds(from).includes(branch)) return {
1812
+ ok: false,
1813
+ reason: "Essa saída não existe mais na condição."
1814
+ };
1815
+ } else if (branch) return {
1816
+ ok: false,
1817
+ reason: "Só a condição tem saídas por regra."
1818
+ };
1819
+ const id = edgeId(source, target, branch);
1820
+ if (flow.edges.some((e) => e.id === id)) return {
1821
+ ok: false,
1822
+ reason: "Essa ligação já existe."
1823
+ };
1824
+ const sameOutput = (e) => e.source === source && (e.branch ?? void 0) === branch;
1825
+ const old = flow.edges.find(sameOutput);
1826
+ const edge = branch ? {
1827
+ id,
1828
+ source,
1829
+ target,
1830
+ branch
1831
+ } : {
1832
+ id,
1833
+ source,
1834
+ target
1835
+ };
1836
+ return {
1837
+ ok: true,
1838
+ flow: {
1839
+ ...flow,
1840
+ edges: [...flow.edges.filter((e) => !sameOutput(e)), edge]
1841
+ },
1842
+ ...old ? { replaced: {
1843
+ source: old.source,
1844
+ target: old.target,
1845
+ ...old.branch ? { branch: old.branch } : {}
1846
+ } } : {}
1847
+ };
1848
+ }
1849
+ /** Blocos que `id` pode ter como próximo: nunca ele mesmo nem um gatilho. */
1850
+ function nextCandidates(flow, id) {
1851
+ if (!flow.nodes.some((n) => n.id === id)) return [];
1852
+ return flow.nodes.filter((n) => n.id !== id && !isTrigger(n.type));
1853
+ }
1854
+ /** Onde o bloco está ligado: quem vem antes, quem vem depois e quantos cairiam junto. */
1855
+ function describeNodeLinks(flow, id) {
1856
+ const byId = new Map(flow.nodes.map((n) => [n.id, n]));
1857
+ const self = byId.get(id);
1858
+ const anteriores = [];
1859
+ const posteriores = [];
1860
+ for (const e of flow.edges) {
1861
+ if (e.target === id) {
1862
+ const node = byId.get(e.source);
1863
+ if (node) anteriores.push({
1864
+ node,
1865
+ ...e.branch ? { saida: branchName(node, e.branch) } : {}
1866
+ });
1867
+ }
1868
+ if (e.source === id && self) {
1869
+ const node = byId.get(e.target);
1870
+ if (node) posteriores.push({
1871
+ node,
1872
+ ...e.branch ? { saida: branchName(self, e.branch) } : {}
1873
+ });
1874
+ }
1875
+ }
1876
+ return {
1877
+ anteriores,
1878
+ posteriores,
1879
+ descendentes: exclusiveDescendants(flow, id).length
1880
+ };
1881
+ }
1882
+ /**
1883
+ * Primeira posição livre a partir de `from`: desce em passos de `FLOW_GAP_Y` e, se a coluna
1884
+ * estiver cheia, passa à coluna da direita. Usa o tamanho medido dos blocos quando há, senão
1885
+ * a estimativa. Determinística: o mesmo fluxo e o mesmo ponto dão sempre a mesma posição.
1886
+ */
1887
+ function findFreePosition(flow, size, from, measured = {}) {
1888
+ const rects = flow.nodes.map((n) => ({
1889
+ x: n.position.x,
1890
+ y: n.position.y,
1891
+ w: measured[n.id]?.w ?? 336,
1892
+ h: measured[n.id]?.h ?? estimateNodeHeight(n)
1893
+ }));
1894
+ const gap = 16;
1895
+ const free = (x, y) => rects.every((r) => x + size.w + gap <= r.x || r.x + r.w + gap <= x || y + size.h + gap <= r.y || r.y + r.h + gap <= y);
1896
+ for (let col = 0; col < 40; col++) {
1897
+ const x = from.x + col * (size.w + 96);
1898
+ for (let row = 0; row < 60; row++) {
1899
+ const y = from.y + row * 32;
1900
+ if (free(x, y)) return {
1901
+ x,
1902
+ y
1903
+ };
1904
+ }
1905
+ }
1906
+ return {
1907
+ x: from.x,
1908
+ y: from.y
1909
+ };
1910
+ }
1911
+ /** Aviso de troca: "A ligação anterior de "X" para "Y" foi trocada." */
1912
+ function replacedMessage(flow, replaced) {
1913
+ const name = (id) => {
1914
+ const n = flow.nodes.find((x) => x.id === id);
1915
+ return n ? nodeName(n) : id;
1916
+ };
1917
+ return `A ligação anterior de "${name(replaced.source)}" para "${name(replaced.target)}" foi trocada.`;
1918
+ }
1919
+ //#endregion
1920
+ //#region src/components/ui/flow-config-panels.tsx
1921
+ var list = asList;
1922
+ var NONE = "__nenhum";
1923
+ function VariableInsert({ variables, label, onInsert }) {
1924
+ return /* @__PURE__ */ jsx(Select, {
1925
+ label: `Inserir variável em ${label}`,
1926
+ placeholder: "Inserir variável",
1927
+ disabled: variables.length === 0,
1928
+ value: null,
1929
+ options: variables.map((v) => ({
1930
+ value: v.id,
1931
+ label: v.id,
1932
+ description: v.label
1933
+ })),
1934
+ onChange: (v) => v && onInsert(`{{${v}}}`)
1935
+ });
1936
+ }
1937
+ /** Campo de texto que aceita variáveis `{{x}}`: o texto e o seletor que acrescenta uma no fim. */
1938
+ function TemplateField({ label, value, onChange, variables, multiline, help }) {
1939
+ return /* @__PURE__ */ jsxs("div", {
1940
+ className: "flex flex-col gap-2",
1941
+ children: [/* @__PURE__ */ jsx(Field, {
1942
+ label,
1943
+ span: "full",
1944
+ help,
1945
+ children: multiline ? /* @__PURE__ */ jsx(Textarea, {
1946
+ rows: 3,
1947
+ value,
1948
+ onChange
1949
+ }) : /* @__PURE__ */ jsx(Input, {
1950
+ value,
1951
+ onChange
1952
+ })
1953
+ }), /* @__PURE__ */ jsx(VariableInsert, {
1954
+ variables,
1955
+ label,
1956
+ onInsert: (token) => onChange(value + token)
1957
+ })]
1958
+ });
1959
+ }
1960
+ /** Escolha de uma variável do fluxo para o corpo do webhook: guarda `{{id}}`. */
1961
+ function VariableSelect({ label, value, variables, onChange }) {
1962
+ const current = /^\{\{\s*([\w.]+)\s*\}\}$/.exec(value)?.[1] ?? null;
1963
+ const options = variables.map((v) => ({
1964
+ value: v.id,
1965
+ label: v.id,
1966
+ description: v.label
1967
+ }));
1968
+ if (current && !variables.some((v) => v.id === current)) options.push({
1969
+ value: current,
1970
+ label: current
1971
+ });
1972
+ return /* @__PURE__ */ jsx(Select, {
1973
+ label,
1974
+ placeholder: "Variável",
1975
+ value: current,
1976
+ options,
1977
+ clearable: true,
1978
+ onChange: (v) => onChange(v ? `{{${v}}}` : "")
1979
+ });
1980
+ }
1981
+ function ConditionFields({ flow, node, variables, onApply }) {
1982
+ const rules = rulesOf(node);
1983
+ const fieldOptions = (rule) => {
1984
+ const opts = variables.map((v) => ({
1985
+ value: v.id,
1986
+ label: v.id,
1987
+ description: v.label
1988
+ }));
1989
+ if (rule.field && !opts.some((o) => o.value === rule.field)) opts.push({
1990
+ value: rule.field,
1991
+ label: rule.field,
1992
+ description: ""
1993
+ });
1994
+ return opts;
1995
+ };
1996
+ const save = (next, base = flow) => onApply(updateNodeData(base, node.id, { config: {
1997
+ ...node.data.config,
1998
+ rules: next
1999
+ } }));
2000
+ const patch = (i, p) => save(rules.map((r, k) => k === i ? {
2001
+ ...r,
2002
+ ...p
2003
+ } : r));
2004
+ const add = () => save([...rules, {
2005
+ id: nextRuleId(rules),
2006
+ field: "",
2007
+ operator: "igual",
2008
+ value: ""
2009
+ }]);
2010
+ const remove = (i) => {
2011
+ const rule = rules[i];
2012
+ let next = flow;
2013
+ for (const e of flow.edges) if (e.source === node.id && e.branch === rule.id) next = removeEdge(next, e.id);
2014
+ save(rules.filter((_, k) => k !== i), next);
2015
+ };
2016
+ return /* @__PURE__ */ jsxs("div", {
2017
+ className: "flex flex-col gap-4",
2018
+ children: [
2019
+ rules.map((rule, i) => /* @__PURE__ */ jsxs("div", {
2020
+ role: "group",
2021
+ "aria-label": branchName(node, rule.id),
2022
+ className: "flex flex-col gap-2 rounded-block border p-3",
2023
+ children: [
2024
+ /* @__PURE__ */ jsxs("div", {
2025
+ className: "flex items-center justify-between gap-2",
2026
+ children: [/* @__PURE__ */ jsx("span", {
2027
+ className: "text-sm font-medium",
2028
+ children: branchName(node, rule.id)
2029
+ }), /* @__PURE__ */ jsx(Button, {
2030
+ variant: "ghost",
2031
+ size: "sm",
2032
+ iconOnly: true,
2033
+ "aria-label": `Excluir ${branchName(node, rule.id)}`,
2034
+ className: "min-h-touch min-w-touch",
2035
+ icon: /* @__PURE__ */ jsx(Trash2, {}),
2036
+ onClick: () => remove(i)
2037
+ })]
2038
+ }),
2039
+ /* @__PURE__ */ jsx(Select, {
2040
+ label: `Campo da regra ${i + 1}`,
2041
+ placeholder: "Campo",
2042
+ options: fieldOptions(rule),
2043
+ value: rule.field || null,
2044
+ onChange: (v) => patch(i, { field: v ?? "" })
2045
+ }),
2046
+ /* @__PURE__ */ jsx(Select, {
2047
+ label: `Operador da regra ${i + 1}`,
2048
+ options: FLOW_RULE_OPERATORS.map((o) => ({
2049
+ value: o.value,
2050
+ label: o.label
2051
+ })),
2052
+ value: rule.operator,
2053
+ onChange: (v) => {
2054
+ const operator = v ?? "igual";
2055
+ patch(i, {
2056
+ operator,
2057
+ ...operatorHasValue(operator) ? {} : { value: "" }
2058
+ });
2059
+ }
2060
+ }),
2061
+ operatorHasValue(rule.operator) && /* @__PURE__ */ jsx(Input, {
2062
+ "aria-label": `Valor da regra ${i + 1}`,
2063
+ placeholder: "Valor",
2064
+ value: rule.value,
2065
+ onChange: (v) => patch(i, { value: v })
2066
+ })
2067
+ ]
2068
+ }, rule.id)),
2069
+ rules.length === 0 && /* @__PURE__ */ jsx(EmptyState, {
2070
+ size: "compact",
2071
+ title: "Nenhuma regra",
2072
+ description: "A condição ainda não tem regras."
2073
+ }),
2074
+ /* @__PURE__ */ jsx(Button, {
2075
+ variant: "outline",
2076
+ size: "sm",
2077
+ icon: /* @__PURE__ */ jsx(Plus, {}),
2078
+ onClick: add,
2079
+ children: "Adicionar regra"
2080
+ }),
2081
+ /* @__PURE__ */ jsxs("p", {
2082
+ className: "rounded-block bg-muted p-3 text-sm",
2083
+ children: [/* @__PURE__ */ jsx("span", {
2084
+ className: "font-medium",
2085
+ children: "Senão"
2086
+ }), /* @__PURE__ */ jsx("span", {
2087
+ className: "text-muted-foreground",
2088
+ children: " segue quando nenhuma regra bater."
2089
+ })]
2090
+ })
2091
+ ]
2092
+ });
2093
+ }
2094
+ function CodeBlock({ children }) {
2095
+ return /* @__PURE__ */ jsx("pre", {
2096
+ className: "rounded-block bg-muted p-3 text-xs break-words whitespace-pre-wrap text-foreground",
2097
+ children
2098
+ });
2099
+ }
2100
+ var ENDPOINT_ORIGIN = "https://app.exemplo.com";
2101
+ function WebhookInFields({ cfg, set }) {
2102
+ const received = cfg.received === true;
2103
+ const mapping = list(cfg.mapping);
2104
+ const sample = flattenPayload(FLOW_SAMPLE_PAYLOAD);
2105
+ const mapOf = (path) => mapping.find((m) => str(m.from) === path);
2106
+ const setMap = (path, to) => set({ mapping: [...mapping.filter((m) => str(m.from) !== path), ...to ? [{
2107
+ from: path,
2108
+ to
2109
+ }] : []] });
2110
+ const copy = async () => {
2111
+ try {
2112
+ await navigator.clipboard.writeText(`${ENDPOINT_ORIGIN}${str(cfg.path)}`);
2113
+ toast.success("Endereço copiado");
2114
+ } catch {
2115
+ toast.error("Não foi possível copiar o endereço");
2116
+ }
2117
+ };
2118
+ return /* @__PURE__ */ jsx(Tabs, {
2119
+ variant: "pill",
2120
+ "aria-label": "Webhook de entrada",
2121
+ items: [{
2122
+ value: "mapeamento",
2123
+ label: "Mapeamento",
2124
+ content: /* @__PURE__ */ jsxs("div", {
2125
+ className: "flex flex-col gap-4 pt-4",
2126
+ children: [
2127
+ /* @__PURE__ */ jsxs("div", {
2128
+ className: "flex items-center gap-2",
2129
+ children: [/* @__PURE__ */ jsx(Button, {
2130
+ variant: "outline",
2131
+ size: "sm",
2132
+ onClick: () => set({ received: true }),
2133
+ children: "Simular chegada"
2134
+ }), /* @__PURE__ */ jsx(InfoHint, {
2135
+ title: "Mapeamento de campos",
2136
+ children: "Simule a chegada de dados de exemplo e ligue cada campo recebido a um campo do sistema. Os campos mapeados viram variáveis para os blocos seguintes."
2137
+ })]
2138
+ }),
2139
+ !received && /* @__PURE__ */ jsx(EmptyState, {
2140
+ size: "compact",
2141
+ title: "Nenhum dado recebido",
2142
+ description: "Os campos aparecem depois da chegada simulada."
2143
+ }),
2144
+ received && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(CodeBlock, { children: JSON.stringify(FLOW_SAMPLE_PAYLOAD, null, 2) }), /* @__PURE__ */ jsx("ul", {
2145
+ "aria-label": "Campos recebidos",
2146
+ className: "flex flex-col gap-3",
2147
+ children: sample.map((f) => /* @__PURE__ */ jsxs("li", {
2148
+ className: "flex flex-col gap-1",
2149
+ children: [/* @__PURE__ */ jsxs("div", {
2150
+ className: "flex items-center justify-between gap-2 text-xs",
2151
+ children: [/* @__PURE__ */ jsx("code", {
2152
+ className: "font-medium",
2153
+ children: f.path
2154
+ }), /* @__PURE__ */ jsx("span", {
2155
+ className: "truncate text-muted-foreground",
2156
+ children: f.value
2157
+ })]
2158
+ }), /* @__PURE__ */ jsx(Select, {
2159
+ label: `Campo do sistema para ${f.path}`,
2160
+ placeholder: "Não mapear",
2161
+ searchable: true,
2162
+ clearable: true,
2163
+ options: FLOW_SYSTEM_FIELDS.map((o) => ({
2164
+ value: o.value,
2165
+ label: o.label
2166
+ })),
2167
+ value: str(mapOf(f.path)?.to) || null,
2168
+ onChange: (v) => setMap(f.path, v)
2169
+ })]
2170
+ }, f.path))
2171
+ })] })
2172
+ ]
2173
+ })
2174
+ }, {
2175
+ value: "configuracoes",
2176
+ label: "Configurações",
2177
+ content: /* @__PURE__ */ jsxs("div", {
2178
+ className: "flex flex-col gap-4 pt-4",
2179
+ children: [
2180
+ /* @__PURE__ */ jsx(Field, {
2181
+ label: "Método",
2182
+ span: "full",
2183
+ children: /* @__PURE__ */ jsx(Select, {
2184
+ label: "Método",
2185
+ options: ["POST", "PUT"].map((m) => ({
2186
+ value: m,
2187
+ label: m
2188
+ })),
2189
+ value: str(cfg.method) || "POST",
2190
+ onChange: (v) => set({ method: v ?? "POST" })
2191
+ })
2192
+ }),
2193
+ /* @__PURE__ */ jsx(Field, {
2194
+ label: "Endereço",
2195
+ span: "full",
2196
+ required: true,
2197
+ children: /* @__PURE__ */ jsx(Input, {
2198
+ value: str(cfg.path),
2199
+ onChange: (v) => set({ path: v })
2200
+ })
2201
+ }),
2202
+ /* @__PURE__ */ jsx(Button, {
2203
+ variant: "outline",
2204
+ size: "sm",
2205
+ icon: /* @__PURE__ */ jsx(Copy, {}),
2206
+ onClick: () => void copy(),
2207
+ children: "Copiar endereço"
2208
+ }),
2209
+ /* @__PURE__ */ jsx(Field, {
2210
+ label: "Código de resposta",
2211
+ span: "full",
2212
+ children: /* @__PURE__ */ jsx(Select, {
2213
+ label: "Código de resposta",
2214
+ options: [
2215
+ {
2216
+ value: "200",
2217
+ label: "200 OK"
2218
+ },
2219
+ {
2220
+ value: "202",
2221
+ label: "202 Accepted"
2222
+ },
2223
+ {
2224
+ value: "204",
2225
+ label: "204 No Content"
2226
+ }
2227
+ ],
2228
+ value: String(cfg.responseCode ?? 200),
2229
+ onChange: (v) => set({ responseCode: Number(v ?? 200) })
2230
+ })
2231
+ })
2232
+ ]
2233
+ })
2234
+ }]
2235
+ });
2236
+ }
2237
+ function WebhookOutFields({ flow, cfg, variables, set }) {
2238
+ const [simulated, setSimulated] = useState(false);
2239
+ const body = list(cfg.body);
2240
+ const headers = list(cfg.headers);
2241
+ const patchBody = (i, p) => set({ body: body.map((b, k) => k === i ? {
2242
+ ...b,
2243
+ ...p
2244
+ } : b) });
2245
+ const patchHeader = (i, p) => set({ headers: headers.map((h, k) => k === i ? {
2246
+ ...h,
2247
+ ...p
2248
+ } : h) });
2249
+ const request = buildWebhookRequest(cfg, flowSampleData(flow));
2250
+ return /* @__PURE__ */ jsx(Tabs, {
2251
+ variant: "pill",
2252
+ "aria-label": "Webhook de saída",
2253
+ items: [{
2254
+ value: "corpo",
2255
+ label: "Corpo",
2256
+ content: /* @__PURE__ */ jsxs("div", {
2257
+ className: "flex flex-col gap-4 pt-4",
2258
+ children: [
2259
+ body.map((b, i) => /* @__PURE__ */ jsxs("div", {
2260
+ role: "group",
2261
+ "aria-label": `Campo enviado ${i + 1}`,
2262
+ className: "flex flex-col gap-2 rounded-block border p-3",
2263
+ children: [/* @__PURE__ */ jsxs("div", {
2264
+ className: "flex items-center gap-2",
2265
+ children: [/* @__PURE__ */ jsx(Input, {
2266
+ "aria-label": `Nome do campo ${i + 1}`,
2267
+ placeholder: "Nome do campo",
2268
+ value: str(b.key),
2269
+ onChange: (v) => patchBody(i, { key: v })
2270
+ }), /* @__PURE__ */ jsx(Button, {
2271
+ variant: "ghost",
2272
+ size: "sm",
2273
+ iconOnly: true,
2274
+ "aria-label": `Excluir campo ${i + 1}`,
2275
+ className: "min-h-touch min-w-touch shrink-0",
2276
+ icon: /* @__PURE__ */ jsx(Trash2, {}),
2277
+ onClick: () => set({ body: body.filter((_, k) => k !== i) })
2278
+ })]
2279
+ }), /* @__PURE__ */ jsx(VariableSelect, {
2280
+ label: `Variável do campo ${i + 1}`,
2281
+ value: str(b.value),
2282
+ variables,
2283
+ onChange: (token) => patchBody(i, { value: token })
2284
+ })]
2285
+ }, i)),
2286
+ /* @__PURE__ */ jsx(Button, {
2287
+ variant: "outline",
2288
+ size: "sm",
2289
+ icon: /* @__PURE__ */ jsx(Plus, {}),
2290
+ onClick: () => set({ body: [...body, {
2291
+ key: "",
2292
+ value: ""
2293
+ }] }),
2294
+ children: "Adicionar campo"
2295
+ }),
2296
+ /* @__PURE__ */ jsxs("div", {
2297
+ className: "flex items-center gap-2",
2298
+ children: [/* @__PURE__ */ jsx(Button, {
2299
+ variant: "outline",
2300
+ size: "sm",
2301
+ onClick: () => setSimulated(true),
2302
+ children: "Simular envio"
2303
+ }), /* @__PURE__ */ jsx(InfoHint, {
2304
+ title: "Simulação do envio",
2305
+ children: "Nada é enviado de verdade: a requisição é montada com os dados de exemplo e a resposta é uma amostra."
2306
+ })]
2307
+ }),
2308
+ simulated && /* @__PURE__ */ jsxs("div", {
2309
+ role: "region",
2310
+ "aria-label": "Simulação do envio",
2311
+ className: "flex flex-col gap-2",
2312
+ children: [/* @__PURE__ */ jsx(CodeBlock, { children: `${request.method} ${request.url}\n${JSON.stringify(request.body, null, 2)}` }), /* @__PURE__ */ jsx(CodeBlock, { children: "200 OK { \"id\": \"crm-88\" }" })]
2313
+ })
2314
+ ]
2315
+ })
2316
+ }, {
2317
+ value: "configuracoes",
2318
+ label: "Configurações",
2319
+ content: /* @__PURE__ */ jsxs("div", {
2320
+ className: "flex flex-col gap-4 pt-4",
2321
+ children: [
2322
+ /* @__PURE__ */ jsx(Field, {
2323
+ label: "Método",
2324
+ span: "full",
2325
+ children: /* @__PURE__ */ jsx(Select, {
2326
+ label: "Método",
2327
+ options: [
2328
+ "POST",
2329
+ "PUT",
2330
+ "PATCH"
2331
+ ].map((m) => ({
2332
+ value: m,
2333
+ label: m
2334
+ })),
2335
+ value: str(cfg.method) || "POST",
2336
+ onChange: (v) => set({ method: v ?? "POST" })
2337
+ })
2338
+ }),
2339
+ /* @__PURE__ */ jsx(Field, {
2340
+ label: "Endereço",
2341
+ span: "full",
2342
+ required: true,
2343
+ children: /* @__PURE__ */ jsx(Input, {
2344
+ value: str(cfg.url),
2345
+ onChange: (v) => set({ url: v })
2346
+ })
2347
+ }),
2348
+ headers.map((h, i) => /* @__PURE__ */ jsxs("div", {
2349
+ role: "group",
2350
+ "aria-label": `Cabeçalho ${i + 1}`,
2351
+ className: "flex flex-col gap-2 rounded-block border p-3",
2352
+ children: [/* @__PURE__ */ jsx(Input, {
2353
+ "aria-label": `Nome do cabeçalho ${i + 1}`,
2354
+ placeholder: "Nome",
2355
+ value: str(h.name),
2356
+ onChange: (v) => patchHeader(i, { name: v })
2357
+ }), /* @__PURE__ */ jsxs("div", {
2358
+ className: "flex items-center gap-2",
2359
+ children: [/* @__PURE__ */ jsx(Input, {
2360
+ "aria-label": `Valor do cabeçalho ${i + 1}`,
2361
+ placeholder: "Valor",
2362
+ value: str(h.value),
2363
+ onChange: (v) => patchHeader(i, { value: v })
2364
+ }), /* @__PURE__ */ jsx(Button, {
2365
+ variant: "ghost",
2366
+ size: "sm",
2367
+ iconOnly: true,
2368
+ "aria-label": `Excluir cabeçalho ${i + 1}`,
2369
+ className: "min-h-touch min-w-touch shrink-0",
2370
+ icon: /* @__PURE__ */ jsx(Trash2, {}),
2371
+ onClick: () => set({ headers: headers.filter((_, k) => k !== i) })
2372
+ })]
2373
+ })]
2374
+ }, i)),
2375
+ /* @__PURE__ */ jsx(Button, {
2376
+ variant: "outline",
2377
+ size: "sm",
2378
+ icon: /* @__PURE__ */ jsx(Plus, {}),
2379
+ onClick: () => set({ headers: [...headers, {
2380
+ name: "",
2381
+ value: ""
2382
+ }] }),
2383
+ children: "Adicionar cabeçalho"
2384
+ })
2385
+ ]
2386
+ })
2387
+ }]
943
2388
  });
2389
+ }
2390
+ function LinkFields({ flow, node, onApply }) {
944
2391
  const options = [{
945
2392
  value: NONE,
946
2393
  label: "Nenhum"
@@ -948,240 +2395,644 @@ function BlockDrawer({ flow, nodeId, open, onOpenChange, onApply, onDelete }) {
948
2395
  value: n.id,
949
2396
  label: `${FLOW_TYPE_NAME[n.type]}: ${nodeName(n)}`
950
2397
  }))];
951
- const target = (value) => value && value !== NONE ? value : null;
952
- const apply = () => {
953
- let next = updateNodeData(flow, node.id, {
954
- label: draft.label,
955
- ...node.type === "espera" ? { config: {
956
- duration,
957
- unit: draft.unit
958
- } } : {}
959
- });
960
- if (node.type === "condicao") {
961
- next = setNext(next, node.id, target(draft.sim), "sim");
962
- next = setNext(next, node.id, target(draft.nao), "nao");
963
- } else if (node.type !== "fim") next = setNext(next, node.id, target(draft.next));
964
- onApply(next);
965
- };
966
- const nextField = (name, key) => /* @__PURE__ */ jsx(Field, {
967
- label: name,
968
- span: "full",
969
- children: /* @__PURE__ */ jsx(Select, {
970
- label: name,
971
- options,
972
- value: draft[key],
973
- onChange: (v) => set({ [key]: v ?? NONE })
2398
+ return /* @__PURE__ */ jsx("div", {
2399
+ className: "flex flex-col gap-3",
2400
+ children: outputIds(node).map((out) => {
2401
+ const label = out ? `Próximo bloco: ${branchName(node, out)}` : "Próximo bloco";
2402
+ const current = flow.edges.find((e) => e.source === node.id && (e.branch ?? void 0) === out);
2403
+ return /* @__PURE__ */ jsx(Field, {
2404
+ label,
2405
+ span: "full",
2406
+ children: /* @__PURE__ */ jsx(Select, {
2407
+ label,
2408
+ options,
2409
+ value: current?.target ?? NONE,
2410
+ onChange: (v) => {
2411
+ if (!v || v === NONE) return onApply(clearOutput(flow, node.id, out));
2412
+ const r = connectNodes(flow, node.id, v, out);
2413
+ if (!r.ok) return onApply(flow, r.reason);
2414
+ onApply(r.flow, r.replaced ? replacedMessage(flow, r.replaced) : void 0);
2415
+ }
2416
+ })
2417
+ }, out ?? "unica");
974
2418
  })
975
2419
  });
976
- return /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Drawer, {
977
- open,
978
- onOpenChange,
979
- title: `Bloco: ${nodeName(node)}`,
980
- description: FLOW_TYPE_NAME[node.type],
981
- dirty,
982
- footer: /* @__PURE__ */ jsx(ActionBar, {
983
- primary: {
984
- label: "Aplicar",
985
- onClick: apply,
986
- disabled: draft.label.trim() === "" || Boolean(durationError)
987
- },
988
- cancel: {
989
- label: "Cancelar",
990
- onClick: () => dirty ? setDiscarding(true) : onOpenChange(false)
991
- },
992
- secondary: [{
993
- label: "Excluir bloco",
994
- icon: /* @__PURE__ */ jsx(Trash2, {}),
995
- destructive: true,
996
- onClick: () => onDelete(node.id)
997
- }]
998
- }),
999
- children: /* @__PURE__ */ jsxs("div", {
1000
- className: "flex flex-col gap-4",
1001
- children: [
2420
+ }
2421
+ function BlockConfigPanel({ flow, nodeId, onApply, onClose }) {
2422
+ const uid = useId();
2423
+ const node = flow.nodes.find((n) => n.id === nodeId);
2424
+ if (!node) return null;
2425
+ const cfg = node.data.config ?? {};
2426
+ const variables = flowVariables(flow);
2427
+ const set = (patch) => onApply(updateNodeData(flow, node.id, { config: {
2428
+ ...cfg,
2429
+ ...patch
2430
+ } }));
2431
+ const ctx = {
2432
+ flow,
2433
+ node,
2434
+ cfg,
2435
+ variables,
2436
+ set
2437
+ };
2438
+ const t = (key, label, opts = {}) => /* @__PURE__ */ jsx(TemplateField, {
2439
+ label,
2440
+ multiline: opts.multiline,
2441
+ value: str(cfg[key]),
2442
+ variables,
2443
+ onChange: (v) => set({ [key]: v })
2444
+ }, `${uid}-${key}`);
2445
+ let fields = null;
2446
+ switch (node.type) {
2447
+ case "webhook-entrada":
2448
+ fields = /* @__PURE__ */ jsx(WebhookInFields, { ...ctx });
2449
+ break;
2450
+ case "webhook-saida":
2451
+ fields = /* @__PURE__ */ jsx(WebhookOutFields, { ...ctx });
2452
+ break;
2453
+ case "agendamento":
2454
+ fields = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Field, {
2455
+ label: "Frequência",
2456
+ span: "full",
2457
+ children: /* @__PURE__ */ jsx(Select, {
2458
+ label: "Frequência",
2459
+ options: [
2460
+ {
2461
+ value: "diario",
2462
+ label: "Diário"
2463
+ },
2464
+ {
2465
+ value: "semanal",
2466
+ label: "Semanal"
2467
+ },
2468
+ {
2469
+ value: "mensal",
2470
+ label: "Mensal"
2471
+ }
2472
+ ],
2473
+ value: str(cfg.frequency) || "diario",
2474
+ onChange: (v) => set({ frequency: v ?? "diario" })
2475
+ })
2476
+ }), /* @__PURE__ */ jsx(Field, {
2477
+ label: "Horário",
2478
+ span: "full",
2479
+ required: true,
2480
+ children: /* @__PURE__ */ jsx(Input, {
2481
+ type: "time",
2482
+ value: str(cfg.time),
2483
+ onChange: (v) => set({ time: v })
2484
+ })
2485
+ })] });
2486
+ break;
2487
+ case "registro-criado":
2488
+ fields = /* @__PURE__ */ jsx(RadioGroup, {
2489
+ "aria-label": "Registro",
2490
+ options: [
2491
+ {
2492
+ value: "cliente",
2493
+ label: "Cliente"
2494
+ },
2495
+ {
2496
+ value: "pedido",
2497
+ label: "Pedido"
2498
+ },
2499
+ {
2500
+ value: "negocio",
2501
+ label: "Negócio"
2502
+ }
2503
+ ],
2504
+ value: str(cfg.record) || "cliente",
2505
+ onChange: (v) => set({ record: v })
2506
+ });
2507
+ break;
2508
+ case "whatsapp":
2509
+ fields = /* @__PURE__ */ jsxs(Fragment$1, { children: [t("to", "Para"), t("message", "Mensagem", { multiline: true })] });
2510
+ break;
2511
+ case "email":
2512
+ fields = /* @__PURE__ */ jsxs(Fragment$1, { children: [
2513
+ t("to", "Para"),
2514
+ t("subject", "Assunto"),
2515
+ t("message", "Mensagem", { multiline: true })
2516
+ ] });
2517
+ break;
2518
+ case "tarefa":
2519
+ fields = /* @__PURE__ */ jsxs(Fragment$1, { children: [
2520
+ t("title", "Título"),
1002
2521
  /* @__PURE__ */ jsx(Field, {
1003
- label: "Rótulo",
2522
+ label: "Responsável",
1004
2523
  span: "full",
1005
- error: draft.label.trim() === "" ? "Informe um rótulo." : void 0,
1006
2524
  children: /* @__PURE__ */ jsx(Input, {
1007
- value: draft.label,
1008
- onChange: (v) => set({ label: v })
2525
+ value: str(cfg.assignee),
2526
+ onChange: (v) => set({ assignee: v })
1009
2527
  })
1010
2528
  }),
1011
- node.type === "espera" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Field, {
1012
- label: "Duração",
2529
+ /* @__PURE__ */ jsx(Field, {
2530
+ label: "Prazo em dias",
1013
2531
  span: "full",
1014
- error: durationError,
1015
2532
  children: /* @__PURE__ */ jsx(Input, {
1016
- value: draft.duration,
1017
2533
  inputMode: "numeric",
1018
- onChange: (v) => set({ duration: v })
2534
+ value: String(cfg.dueDays ?? ""),
2535
+ onChange: (v) => set({ dueDays: Number(v.replace(/\D/g, "")) || 0 })
1019
2536
  })
1020
- }), /* @__PURE__ */ jsx(Field, {
2537
+ })
2538
+ ] });
2539
+ break;
2540
+ case "espera":
2541
+ fields = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Field, {
2542
+ label: "Duração",
2543
+ span: "full",
2544
+ error: typeof cfg.duration === "number" && cfg.duration > 0 ? void 0 : "Informe um número maior que zero.",
2545
+ children: /* @__PURE__ */ jsx(Input, {
2546
+ inputMode: "numeric",
2547
+ value: cfg.duration === void 0 ? "" : String(cfg.duration),
2548
+ onChange: (v) => set({ duration: Number(v.replace(/\D/g, "")) || 0 })
2549
+ })
2550
+ }), /* @__PURE__ */ jsx(Field, {
2551
+ label: "Unidade",
2552
+ span: "full",
2553
+ children: /* @__PURE__ */ jsx(Select, {
1021
2554
  label: "Unidade",
1022
- span: "full",
1023
- children: /* @__PURE__ */ jsx(Select, {
1024
- label: "Unidade",
1025
- options: FLOW_WAIT_UNITS.map((u) => ({
1026
- value: u,
1027
- label: u
1028
- })),
1029
- value: draft.unit,
1030
- onChange: (v) => set({ unit: v ?? "minutos" })
1031
- })
1032
- })] }),
1033
- node.type === "condicao" && /* @__PURE__ */ jsxs(Fragment$1, { children: [nextField("Se sim", "sim"), nextField("Se não", "nao")] }),
1034
- node.type !== "condicao" && node.type !== "fim" && nextField("Próximo bloco", "next")
1035
- ]
2555
+ options: FLOW_WAIT_UNITS.map((u) => ({
2556
+ value: u,
2557
+ label: u
2558
+ })),
2559
+ value: str(cfg.unit) || "minutos",
2560
+ onChange: (v) => set({ unit: v ?? "minutos" })
2561
+ })
2562
+ })] });
2563
+ break;
2564
+ case "condicao": fields = /* @__PURE__ */ jsx(ConditionFields, {
2565
+ ...ctx,
2566
+ onApply
2567
+ });
2568
+ }
2569
+ return /* @__PURE__ */ jsxs(FlowPanelShell, {
2570
+ title: FLOW_BLOCKS[node.type].name,
2571
+ onClose,
2572
+ closeLabel: "Fechar configuração",
2573
+ children: [
2574
+ /* @__PURE__ */ jsx(Field, {
2575
+ label: "Nome do bloco",
2576
+ span: "full",
2577
+ children: /* @__PURE__ */ jsx(Input, {
2578
+ value: node.data.label,
2579
+ onChange: (v) => onApply(updateNodeData(flow, node.id, { label: v }))
2580
+ })
2581
+ }),
2582
+ fields,
2583
+ /* @__PURE__ */ jsxs("section", {
2584
+ "aria-label": "Ligações",
2585
+ className: "flex flex-col gap-3 border-t pt-4",
2586
+ children: [/* @__PURE__ */ jsx("h3", {
2587
+ className: "text-xs font-medium text-muted-foreground",
2588
+ children: "Ligações"
2589
+ }), /* @__PURE__ */ jsx(LinkFields, {
2590
+ flow,
2591
+ node,
2592
+ onApply
2593
+ })]
2594
+ })
2595
+ ]
2596
+ });
2597
+ }
2598
+ //#endregion
2599
+ //#region src/lib/flow-sim.ts
2600
+ function resetSimulation() {
2601
+ return {
2602
+ status: "idle",
2603
+ steps: [],
2604
+ currentId: null,
2605
+ issues: []
2606
+ };
2607
+ }
2608
+ /** Começa no gatilho. Fluxo inválido não inicia: devolve `error` com os problemas. */
2609
+ function startSimulation(input) {
2610
+ const flow = migrateFlow(input);
2611
+ const issues = validateFlow(flow);
2612
+ if (issues.length > 0) return {
2613
+ status: "error",
2614
+ steps: [],
2615
+ currentId: null,
2616
+ issues
2617
+ };
2618
+ return {
2619
+ status: "running",
2620
+ steps: [],
2621
+ currentId: flow.nodes.find((n) => isTrigger(n.type)).id,
2622
+ issues: []
2623
+ };
2624
+ }
2625
+ /** Avalia uma regra contra os dados de exemplo; campo que não existe conta como vazio. */
2626
+ function evaluateRule(rule, data) {
2627
+ const actual = data[rule.field] ?? "";
2628
+ const wanted = rule.value;
2629
+ switch (rule.operator) {
2630
+ case "igual": return actual === wanted;
2631
+ case "diferente": return actual !== wanted;
2632
+ case "maior":
2633
+ case "menor": {
2634
+ const a = Number(actual);
2635
+ const b = Number(wanted);
2636
+ if (actual.trim() === "" || wanted.trim() === "" || Number.isNaN(a) || Number.isNaN(b)) return false;
2637
+ return rule.operator === "maior" ? a > b : a < b;
2638
+ }
2639
+ case "contem": return actual.includes(wanted);
2640
+ case "vazio": return actual === "";
2641
+ case "nao-vazio": return actual !== "";
2642
+ }
2643
+ }
2644
+ var FREQUENCY = {
2645
+ diario: "todo dia",
2646
+ semanal: "toda semana",
2647
+ mensal: "todo mês"
2648
+ };
2649
+ /** A regra que bate primeiro (ou `null`, e o caminho segue por "Senão"). */
2650
+ function matchedRule(node, data) {
2651
+ const rules = rulesOf(node);
2652
+ const index = rules.findIndex((r) => evaluateRule(r, data));
2653
+ return {
2654
+ rule: index < 0 ? null : rules[index],
2655
+ index
2656
+ };
2657
+ }
2658
+ function conditionDetail(node, data) {
2659
+ const { rule, index } = matchedRule(node, data);
2660
+ return rule ? `Regra ${index + 1} bateu: ${ruleSummary(rule)}, seguiu por ela` : "Nenhuma regra bateu, seguiu por Senão";
2661
+ }
2662
+ function blockDetail(node, data) {
2663
+ const c = node.data.config;
2664
+ switch (node.type) {
2665
+ case "webhook-entrada": return `Receberia ${str(c?.method) || "POST"} em ${str(c?.path)}`;
2666
+ case "agendamento": return `Iniciaria ${FREQUENCY[str(c?.frequency)] ?? ""} às ${str(c?.time)}`.replace(" ", " ");
2667
+ case "registro-criado": return `Iniciaria quando um ${(RECORD_LABEL[str(c?.record)] ?? "registro").toLowerCase()} fosse criado`;
2668
+ case "whatsapp": return `Enviaria WhatsApp para ${renderTemplate(str(c?.to), data)}: "${renderTemplate(str(c?.message), data)}"`;
2669
+ case "email": return `Enviaria e-mail para ${renderTemplate(str(c?.to), data)}, assunto "${renderTemplate(str(c?.subject), data)}": "${renderTemplate(str(c?.message), data)}"`;
2670
+ case "espera": return describeWait(c);
2671
+ case "webhook-saida": {
2672
+ const r = buildWebhookRequest(c, data);
2673
+ return `Enviaria ${r.method} ${r.url} com ${JSON.stringify(r.body)}`;
2674
+ }
2675
+ case "tarefa": {
2676
+ const days = typeof c?.dueDays === "number" ? c.dueDays : 0;
2677
+ return `Criaria a tarefa "${renderTemplate(str(c?.title), data)}" para ${str(c?.assignee) || "ninguém"} com prazo de ${days} ${days === 1 ? "dia" : "dias"}`;
2678
+ }
2679
+ case "condicao": return conditionDetail(node, data);
2680
+ default: return "Executaria o bloco";
2681
+ }
2682
+ }
2683
+ var stepOf = (node, detail) => ({
2684
+ nodeId: node.id,
2685
+ type: node.type,
2686
+ label: nodeName(node),
2687
+ status: "succeeded",
2688
+ detail
2689
+ });
2690
+ /** Roda só um bloco, com os mesmos dados de exemplo, e devolve o passo (o play do card). */
2691
+ function simulateBlock(input, nodeId) {
2692
+ const flow = migrateFlow(input);
2693
+ const node = flow.nodes.find((n) => n.id === nodeId);
2694
+ return node ? stepOf(node, blockDetail(node, flowSampleData(flow))) : null;
2695
+ }
2696
+ /** Executa o bloco ativo (um passo) e passa ao próximo. A condição escolhe sozinha a saída. */
2697
+ function advanceSimulation(input, state) {
2698
+ if (state.status !== "running" || !state.currentId) return state;
2699
+ const flow = migrateFlow(input);
2700
+ const node = flow.nodes.find((n) => n.id === state.currentId);
2701
+ const fail = () => ({
2702
+ ...state,
2703
+ status: "error",
2704
+ currentId: null,
2705
+ issues: [{
2706
+ code: "fluxo-mudou",
2707
+ message: "O fluxo mudou durante o teste. Reinicie para testar de novo."
2708
+ }]
2709
+ });
2710
+ if (!node) return fail();
2711
+ const data = flowSampleData(flow);
2712
+ let output;
2713
+ if (node.type === "condicao") output = matchedRule(node, data).rule?.id ?? "senao";
2714
+ const edge = flow.edges.find((e) => e.source === node.id && (e.branch ?? void 0) === output);
2715
+ const target = edge ? flow.nodes.find((n) => n.id === edge.target) : void 0;
2716
+ if (edge && !target) return fail();
2717
+ const detail = blockDetail(node, data);
2718
+ const step = stepOf(node, target ? detail : `${detail}. Caminho encerrado`);
2719
+ const steps = [...state.steps, step];
2720
+ if (!target) return {
2721
+ status: "finished",
2722
+ steps,
2723
+ currentId: null,
2724
+ issues: []
2725
+ };
2726
+ return {
2727
+ status: "running",
2728
+ steps,
2729
+ currentId: target.id,
2730
+ issues: []
2731
+ };
2732
+ }
2733
+ //#endregion
2734
+ //#region src/components/ui/flow-simulation-panel.tsx
2735
+ function IssueList({ flow, issues, onViewBlock }) {
2736
+ return /* @__PURE__ */ jsx("ul", {
2737
+ className: "flex flex-col gap-2",
2738
+ children: issues.map((i, k) => {
2739
+ const node = flow.nodes.find((n) => n.id === i.nodeId);
2740
+ return /* @__PURE__ */ jsxs("li", {
2741
+ className: "flex flex-wrap items-center justify-between gap-2 rounded-control border bg-card px-3 py-2 text-sm",
2742
+ children: [/* @__PURE__ */ jsx("span", { children: i.message }), i.nodeId && /* @__PURE__ */ jsx(Button, {
2743
+ size: "sm",
2744
+ variant: "outline",
2745
+ "aria-label": `Ver bloco ${node ? nodeName(node) : i.nodeId}`,
2746
+ onClick: () => onViewBlock(i.nodeId),
2747
+ children: "Ver bloco"
2748
+ })]
2749
+ }, `${i.code}-${i.nodeId ?? i.edgeId ?? k}-${k}`);
1036
2750
  })
1037
- }), /* @__PURE__ */ jsx(Modal, {
1038
- open: discarding,
1039
- onOpenChange: setDiscarding,
1040
- type: "destructive",
1041
- title: "Descartar alterações?",
1042
- description: "As informações preenchidas neste painel serão perdidas.",
1043
- confirmLabel: "Descartar",
1044
- cancelLabel: "Continuar editando",
1045
- onConfirm: () => onOpenChange(false)
1046
- })] });
1047
- }
1048
- function SimulationPanel({ flow, sim, onSim, onClose }) {
1049
- const waiting = sim.status === "waiting-choice";
1050
- const current = flow.nodes.find((n) => n.id === sim.currentId);
1051
- const events = sim.steps.map((s, i) => ({
2751
+ });
2752
+ }
2753
+ function FlowSimulationPanel({ flow, sim, blockStep, onSim, onClose, onViewBlock }) {
2754
+ const events = (sim.steps.length === 0 && blockStep ? [blockStep] : sim.steps).map((s, i) => ({
1052
2755
  id: `${i}-${s.nodeId}`,
1053
2756
  title: s.label,
1054
2757
  description: s.detail,
1055
- date: `Passo ${i + 1}`,
2758
+ date: blockStep && sim.steps.length === 0 ? "Só este bloco" : `Passo ${i + 1}`,
1056
2759
  status: s.status
1057
2760
  }));
1058
- const choose = (choice) => onSim(advanceSimulation(flow, sim, choice));
1059
- return /* @__PURE__ */ jsxs(Card, {
1060
- role: "region",
1061
- "aria-label": "Teste do fluxo",
1062
- className: "min-w-0 shrink-0 lg:w-popover",
1063
- children: [/* @__PURE__ */ jsxs(CardHeader, {
1064
- actions: /* @__PURE__ */ jsx(Button, {
1065
- size: "sm",
1066
- variant: "ghost",
1067
- iconOnly: true,
1068
- "aria-label": "Fechar teste",
1069
- icon: /* @__PURE__ */ jsx(X, {}),
1070
- onClick: onClose
2761
+ return /* @__PURE__ */ jsxs(FlowPanelShell, {
2762
+ title: "Teste do fluxo",
2763
+ onClose,
2764
+ closeLabel: "Fechar teste",
2765
+ children: [
2766
+ /* @__PURE__ */ jsx(ActionBar, {
2767
+ primary: {
2768
+ label: "Avançar",
2769
+ disabled: sim.status !== "running",
2770
+ onClick: () => onSim(advanceSimulation(flow, sim))
2771
+ },
2772
+ cancel: {
2773
+ label: "Reiniciar",
2774
+ onClick: () => onSim(startSimulation(flow))
2775
+ }
2776
+ }),
2777
+ sim.status === "error" && /* @__PURE__ */ jsxs("div", {
2778
+ className: "flex flex-col gap-3",
2779
+ children: [/* @__PURE__ */ jsx(Alert, {
2780
+ type: "error",
2781
+ title: "O fluxo precisa de ajustes antes do teste"
2782
+ }), /* @__PURE__ */ jsx(IssueList, {
2783
+ flow,
2784
+ issues: sim.issues,
2785
+ onViewBlock
2786
+ })]
2787
+ }),
2788
+ sim.status === "finished" && /* @__PURE__ */ jsxs("p", {
2789
+ role: "status",
2790
+ className: "flex items-center gap-2 text-sm font-medium text-success-soft-foreground",
2791
+ children: [/* @__PURE__ */ jsx(CheckCircle2, {
2792
+ className: "size-icon-md",
2793
+ "aria-hidden": true
2794
+ }), "Fluxo concluído"]
1071
2795
  }),
1072
- children: [/* @__PURE__ */ jsx(CardTitle, { children: "Teste do fluxo" }), /* @__PURE__ */ jsx(CardDescription, { children: "Um passo por vez, sem executar nada de verdade." })]
1073
- }), /* @__PURE__ */ jsxs(CardContent, {
1074
- className: "flex flex-col gap-4",
2796
+ events.length > 0 && /* @__PURE__ */ jsx(Timeline, { events }),
2797
+ events.length === 0 && sim.status !== "error" && /* @__PURE__ */ jsx("p", {
2798
+ className: "text-sm text-muted-foreground",
2799
+ children: "Nenhum passo executado ainda."
2800
+ })
2801
+ ]
2802
+ });
2803
+ }
2804
+ function FlowValidationPanel({ flow, issues, onClose, onViewBlock }) {
2805
+ return /* @__PURE__ */ jsx(FlowPanelShell, {
2806
+ title: "Validação do fluxo",
2807
+ onClose,
2808
+ closeLabel: "Fechar validação",
2809
+ children: /* @__PURE__ */ jsx("div", {
2810
+ "aria-live": "polite",
2811
+ className: "flex flex-col gap-3",
2812
+ children: issues.length === 0 ? /* @__PURE__ */ jsx(Alert, {
2813
+ type: "success",
2814
+ title: "Fluxo válido"
2815
+ }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Alert, {
2816
+ type: "error",
2817
+ title: issues.length === 1 ? "1 problema encontrado" : `${issues.length} problemas encontrados`
2818
+ }), /* @__PURE__ */ jsx(IssueList, {
2819
+ flow,
2820
+ issues,
2821
+ onViewBlock
2822
+ })] })
2823
+ })
2824
+ });
2825
+ }
2826
+ //#endregion
2827
+ //#region src/components/ui/flow-editor.tsx
2828
+ var DRAG_START = 4;
2829
+ var NO_BLOCK = "Exclua um bloco por vez.";
2830
+ /** Reaproveita os objetos de nó que não mudaram (a biblioteca guarda medida e seleção neles). */
2831
+ function mergeNodes(prev, next) {
2832
+ const old = new Map(prev.map((n) => [n.id, n]));
2833
+ return next.map((n) => {
2834
+ const o = old.get(n.id);
2835
+ if (!o) return n;
2836
+ const a = o.data;
2837
+ const b = n.data;
2838
+ if (!o.dragging && o.position.x === n.position.x && o.position.y === n.position.y && o.ariaLabel === n.ariaLabel && a.label === b.label && a.kind === b.kind && a.config === b.config && a.active === b.active && a.issues.length === b.issues.length && a.issues.every((m, i) => m === b.issues[i])) return o;
2839
+ return {
2840
+ ...o,
2841
+ position: o.dragging ? o.position : n.position,
2842
+ ariaLabel: n.ariaLabel,
2843
+ data: b
2844
+ };
2845
+ });
2846
+ }
2847
+ function mergeEdges(prev, next) {
2848
+ const old = new Map(prev.map((e) => [e.id, e]));
2849
+ return next.map((e) => {
2850
+ const o = old.get(e.id);
2851
+ if (!o) return e;
2852
+ return o.source === e.source && o.target === e.target && o.sourceHandle === e.sourceHandle && o.ariaLabel === e.ariaLabel && o.data?.label === e.data?.label ? o : {
2853
+ ...o,
2854
+ ...e
2855
+ };
2856
+ });
2857
+ }
2858
+ /**
2859
+ * Excluir bloco: mostra o bloco, onde ele está ligado e duas opções (só este, ou este e tudo o
2860
+ * que vem depois), com dupla verificação: a caixa CONFIRMO na primeira, a palavra digitada na
2861
+ * segunda. O Modal fica como está: sem a verificação, o botão avisa e o modal continua aberto.
2862
+ */
2863
+ function DeleteBlockModal({ flow, nodeId, onClose, onConfirm }) {
2864
+ const node = flow.nodes.find((n) => n.id === nodeId);
2865
+ const [mode, setMode] = useState("so-este");
2866
+ const [checked, setChecked] = useState(false);
2867
+ const [typed, setTyped] = useState("");
2868
+ const [error, setError] = useState(false);
2869
+ const keepOpen = useRef(false);
2870
+ if (!node) return null;
2871
+ const links = describeNodeLinks(flow, node.id);
2872
+ const total = links.descendentes + 1;
2873
+ const verified = mode === "so-este" ? checked : typed.trim().toLowerCase() === "confirmo";
2874
+ const list = (items, empty) => items.length === 0 ? /* @__PURE__ */ jsx("span", {
2875
+ className: "text-muted-foreground",
2876
+ children: empty
2877
+ }) : /* @__PURE__ */ jsx("ul", {
2878
+ className: "flex flex-col gap-1",
2879
+ children: items.map((x) => /* @__PURE__ */ jsxs("li", { children: [nodeName(x.node), x.saida ? /* @__PURE__ */ jsxs("span", {
2880
+ className: "text-muted-foreground",
1075
2881
  children: [
1076
- /* @__PURE__ */ jsx(ActionBar, {
1077
- primary: {
1078
- label: "Avançar",
1079
- disabled: sim.status !== "running",
1080
- onClick: () => onSim(advanceSimulation(flow, sim))
1081
- },
1082
- cancel: {
1083
- label: "Reiniciar",
1084
- onClick: () => onSim(startSimulation(flow))
1085
- }
2882
+ " (saída ",
2883
+ x.saida,
2884
+ ")"
2885
+ ]
2886
+ }) : null] }, x.node.id))
2887
+ });
2888
+ return /* @__PURE__ */ jsx(Modal, {
2889
+ open: true,
2890
+ size: "md",
2891
+ type: "destructive",
2892
+ title: "Excluir este bloco?",
2893
+ description: nodeName(node) === FLOW_TYPE_NAME[node.type] ? nodeName(node) : `${FLOW_TYPE_NAME[node.type]}: ${nodeName(node)}`,
2894
+ confirmLabel: "Excluir",
2895
+ onOpenChange: (open) => {
2896
+ if (open) return;
2897
+ if (keepOpen.current) {
2898
+ keepOpen.current = false;
2899
+ return;
2900
+ }
2901
+ onClose();
2902
+ },
2903
+ onConfirm: () => {
2904
+ if (!verified) {
2905
+ keepOpen.current = true;
2906
+ setError(true);
2907
+ return;
2908
+ }
2909
+ onConfirm(mode);
2910
+ },
2911
+ children: /* @__PURE__ */ jsxs("div", {
2912
+ className: "flex w-full flex-col gap-4 text-left text-sm",
2913
+ children: [
2914
+ /* @__PURE__ */ jsx("p", {
2915
+ className: "text-muted-foreground",
2916
+ children: FLOW_BLOCKS[node.type].description
1086
2917
  }),
1087
- sim.status === "error" && /* @__PURE__ */ jsxs("div", {
1088
- className: "flex flex-col gap-3",
1089
- children: [/* @__PURE__ */ jsx(Alert, {
1090
- type: "error",
1091
- title: "O fluxo precisa de ajustes antes do teste"
1092
- }), /* @__PURE__ */ jsx("ul", {
1093
- className: "flex list-disc flex-col gap-1 pl-4 text-sm",
1094
- children: sim.issues.map((i, k) => /* @__PURE__ */ jsx("li", { children: i.message }, `${i.code}-${k}`))
1095
- })]
2918
+ /* @__PURE__ */ jsxs("dl", {
2919
+ className: "flex flex-col gap-2",
2920
+ children: [/* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
2921
+ className: "text-xs text-muted-foreground",
2922
+ children: "Ligado depois de"
2923
+ }), /* @__PURE__ */ jsx("dd", { children: list(links.anteriores, "Nenhum bloco antes") })] }), /* @__PURE__ */ jsxs("div", { children: [/* @__PURE__ */ jsx("dt", {
2924
+ className: "text-xs text-muted-foreground",
2925
+ children: "Ligado antes de"
2926
+ }), /* @__PURE__ */ jsx("dd", { children: list(links.posteriores, "Nenhum bloco depois") })] })]
1096
2927
  }),
1097
- waiting && current && /* @__PURE__ */ jsxs("div", {
1098
- role: "group",
1099
- "aria-label": "Escolha o caminho da condição",
1100
- className: "flex flex-col gap-3",
1101
- children: [/* @__PURE__ */ jsx("p", {
1102
- className: "text-sm font-medium",
1103
- children: nodeName(current)
1104
- }), /* @__PURE__ */ jsxs("div", {
1105
- className: "grid grid-actions-2 gap-3",
1106
- children: [/* @__PURE__ */ jsx(Button, {
1107
- variant: "outline",
1108
- onClick: () => choose("sim"),
1109
- children: "Sim"
1110
- }), /* @__PURE__ */ jsx(Button, {
1111
- variant: "outline",
1112
- onClick: () => choose("nao"),
1113
- children: "Não"
1114
- })]
1115
- })]
2928
+ /* @__PURE__ */ jsx(RadioGroup, {
2929
+ "aria-label": "O que excluir",
2930
+ value: mode,
2931
+ onChange: (v) => {
2932
+ setMode(v);
2933
+ setError(false);
2934
+ },
2935
+ options: [{
2936
+ value: "so-este",
2937
+ label: "Excluir só este bloco"
2938
+ }, {
2939
+ value: "daqui-para-frente",
2940
+ label: `Excluir este bloco e tudo o que vem depois dele (${total} ${total === 1 ? "bloco" : "blocos"})`
2941
+ }]
1116
2942
  }),
1117
- sim.status === "finished" && /* @__PURE__ */ jsxs("p", {
1118
- role: "status",
1119
- className: "flex items-center gap-2 text-sm font-medium text-success-soft-foreground",
1120
- children: [/* @__PURE__ */ jsx(CheckCircle2, {
1121
- className: "size-icon-md",
1122
- "aria-hidden": true
1123
- }), "Fluxo concluído"]
2943
+ mode === "so-este" ? /* @__PURE__ */ jsx(Checkbox, {
2944
+ checked,
2945
+ onCheckedChange: (v) => {
2946
+ setChecked(v === true);
2947
+ setError(false);
2948
+ },
2949
+ label: "CONFIRMO"
2950
+ }) : /* @__PURE__ */ jsx(Field, {
2951
+ label: "Digite CONFIRMO para excluir",
2952
+ span: "full",
2953
+ children: /* @__PURE__ */ jsx(Input, {
2954
+ value: typed,
2955
+ onChange: (v) => {
2956
+ setTyped(v);
2957
+ setError(false);
2958
+ }
2959
+ })
1124
2960
  }),
1125
- events.length > 0 && /* @__PURE__ */ jsx(Timeline, { events }),
1126
- events.length === 0 && sim.status !== "error" && /* @__PURE__ */ jsx("p", {
1127
- className: "text-sm text-muted-foreground",
1128
- children: "Nenhum passo executado ainda."
2961
+ error && /* @__PURE__ */ jsx("p", {
2962
+ role: "alert",
2963
+ className: "font-medium text-destructive",
2964
+ children: mode === "so-este" ? "Marque a caixa CONFIRMO para excluir." : "Digite CONFIRMO para excluir."
1129
2965
  })
1130
2966
  ]
1131
- })]
2967
+ })
1132
2968
  });
1133
2969
  }
1134
- function FlowEditorInner({ value, onChange, readOnly = false, className }) {
2970
+ function FlowEditorInner({ value, onChange, readOnly = false, toolbar = true, testOpen: testOpenProp, onTestOpenChange, className }) {
1135
2971
  const { isMobile } = useBreakpoint();
1136
2972
  const canEdit = !readOnly;
1137
2973
  const canBuild = canEdit && !isMobile;
1138
2974
  const { screenToFlowPosition, fitView } = useReactFlow();
2975
+ const updateNodeInternals = useUpdateNodeInternals();
2976
+ const rootRef = useFillHeight();
1139
2977
  const canvasRef = useRef(null);
1140
2978
  const noticeId = useId();
1141
- const valueRef = useRef(value);
1142
- valueRef.current = value;
2979
+ const flow = useMemo(() => migrateFlow(value), [value]);
2980
+ const flowRef = useRef(flow);
2981
+ flowRef.current = flow;
1143
2982
  const everUsed = useRef(/* @__PURE__ */ new Set());
1144
- value.nodes.forEach((n) => everUsed.current.add(n.id));
1145
- const [validated, setValidated] = useState(false);
2983
+ flow.nodes.forEach((n) => everUsed.current.add(n.id));
2984
+ const [view, setView] = useState("blocks");
2985
+ const [configId, setConfigId] = useState(null);
2986
+ const [testState, setTestState] = useState(false);
2987
+ const testOpen = testOpenProp ?? testState;
1146
2988
  const [sim, setSim] = useState(resetSimulation);
2989
+ const [blockStep, setBlockStep] = useState(null);
2990
+ const blockStepRef = useRef(null);
2991
+ blockStepRef.current = blockStep;
2992
+ const [validated, setValidated] = useState(false);
1147
2993
  const [notice, setNotice] = useState(null);
1148
- const [drawer, setDrawer] = useState(null);
1149
2994
  const [drawerOpen, setDrawerOpen] = useState(false);
1150
- const drawerOpenRef = useRef(false);
1151
- drawerOpenRef.current = drawerOpen;
1152
2995
  const [deleting, setDeleting] = useState(null);
2996
+ const deletingRef = useRef(null);
2997
+ deletingRef.current = deleting;
1153
2998
  const [drag, setDrag] = useState(null);
1154
2999
  const suppressClick = useRef(false);
1155
- const issues = useMemo(() => validateFlow(value), [value]);
3000
+ const issues = useMemo(() => validateFlow(flow), [flow]);
1156
3001
  const shownIssues = validated ? issues : void 0;
1157
- const activeId = sim.status === "running" || sim.status === "waiting-choice" ? sim.currentId : null;
1158
- const nextNodes = useMemo(() => toRfNodes(value, {
3002
+ const activeId = sim.status === "running" ? sim.currentId : null;
3003
+ const configNode = configId ? flow.nodes.find((n) => n.id === configId) : void 0;
3004
+ const setTestOpen = useCallback((open) => {
3005
+ setTestState(open);
3006
+ onTestOpenChange?.(open);
3007
+ }, [onTestOpenChange]);
3008
+ const nextNodes = useMemo(() => toRfNodes(flow, {
1159
3009
  activeId,
1160
- issues: shownIssues,
1161
- handles: canBuild ? "lg" : "sm",
1162
- edit: canBuild
3010
+ issues: shownIssues
1163
3011
  }), [
1164
- value,
3012
+ flow,
1165
3013
  activeId,
1166
- shownIssues,
1167
- canBuild
3014
+ shownIssues
1168
3015
  ]);
1169
- const nextEdges = useMemo(() => toRfEdges(value), [value]);
3016
+ const nextEdges = useMemo(() => toRfEdges(flow), [flow]);
1170
3017
  const [rfNodes, setRfNodes] = useState(nextNodes);
1171
3018
  const [rfEdges, setRfEdges] = useState(nextEdges);
1172
3019
  useEffect(() => setRfNodes((prev) => mergeNodes(prev, nextNodes)), [nextNodes]);
1173
3020
  useEffect(() => setRfEdges((prev) => mergeEdges(prev, nextEdges)), [nextEdges]);
1174
- const emit = useCallback((flow) => {
1175
- setNotice(null);
1176
- onChange(flow);
3021
+ const ruleKey = flow.nodes.filter((n) => n.type === "condicao").map((n) => `${n.id}:${rulesOf(n).map((r) => r.id).join(",")}`).join("|");
3022
+ useEffect(() => {
3023
+ for (const part of ruleKey.split("|")) if (part) updateNodeInternals(part.split(":")[0]);
3024
+ }, [ruleKey, updateNodeInternals]);
3025
+ const emit = useCallback((next, message) => {
3026
+ setNotice(message ?? null);
3027
+ onChange(next);
1177
3028
  }, [onChange]);
1178
3029
  const commitMoves = useCallback((moves) => {
1179
- let flow = valueRef.current;
3030
+ let next = flowRef.current;
1180
3031
  for (const m of moves) {
1181
- const cur = flow.nodes.find((n) => n.id === m.id);
1182
- if (cur && (cur.position.x !== m.position.x || cur.position.y !== m.position.y)) flow = moveNode(flow, m.id, m.position);
3032
+ const cur = next.nodes.find((n) => n.id === m.id);
3033
+ if (cur && (cur.position.x !== m.position.x || cur.position.y !== m.position.y)) next = moveNode(next, m.id, m.position);
1183
3034
  }
1184
- if (flow !== valueRef.current) emit(flow);
3035
+ if (next !== flowRef.current) emit(next);
1185
3036
  }, [emit]);
1186
3037
  const onNodesChange = useCallback((changes) => {
1187
3038
  setRfNodes((nds) => applyNodeChanges(changes.filter((c) => c.type !== "remove"), nds));
@@ -1192,38 +3043,88 @@ function FlowEditorInner({ value, onChange, readOnly = false, className }) {
1192
3043
  if (moves.length > 0 && canBuild) commitMoves(moves);
1193
3044
  }, [canBuild, commitMoves]);
1194
3045
  const onEdgesChange = useCallback((changes) => setRfEdges((eds) => applyEdgeChanges(changes.filter((c) => c.type !== "remove"), eds)), []);
1195
- const onConnect = useCallback((c) => {
1196
- if (!c.source || !c.target) return;
1197
- const branch = c.sourceHandle === "sim" || c.sourceHandle === "nao" ? c.sourceHandle : void 0;
1198
- const r = connectNodes(valueRef.current, c.source, c.target, branch);
1199
- if (r.ok) emit(r.flow);
1200
- else setNotice(r.reason);
3046
+ /** Liga e avisa: o motivo da recusa, ou a ligação trocada. */
3047
+ const connect = useCallback((source, target, branch) => {
3048
+ const r = connectNodes(flowRef.current, source, target, branch);
3049
+ if (!r.ok) setNotice(r.reason);
3050
+ else emit(r.flow, r.replaced ? replacedMessage(flowRef.current, r.replaced) : void 0);
1201
3051
  }, [emit]);
3052
+ const onConnect = useCallback((c) => {
3053
+ if (c.source && c.target) connect(c.source, c.target, c.sourceHandle ?? void 0);
3054
+ }, [connect]);
3055
+ /** Soltar em qualquer ponto do bloco liga; fora de qualquer bloco, nada acontece. */
3056
+ const onConnectEnd = useCallback((event, state) => {
3057
+ if (state.isValid || !state.fromNode || state.fromHandle?.type !== "source") return;
3058
+ let to = state.toNode?.id;
3059
+ if (!to && typeof document.elementFromPoint === "function") {
3060
+ const p = "changedTouches" in event ? event.changedTouches[0] : event;
3061
+ if (p) to = document.elementFromPoint(p.clientX, p.clientY)?.closest(".react-flow__node")?.getAttribute("data-id") ?? void 0;
3062
+ }
3063
+ if (!to) return;
3064
+ connect(state.fromNode.id, to, state.fromHandle.id ?? void 0);
3065
+ }, [connect]);
3066
+ const showDrawer = () => {
3067
+ if (isMobile) setDrawerOpen(true);
3068
+ };
3069
+ const openBlocks = () => {
3070
+ setView("blocks");
3071
+ setTestOpen(false);
3072
+ showDrawer();
3073
+ };
3074
+ const openConfig = useCallback((id) => {
3075
+ if (!canEdit) return;
3076
+ setConfigId(id);
3077
+ setView("config");
3078
+ setTestOpen(false);
3079
+ if (isMobile) setDrawerOpen(true);
3080
+ }, [
3081
+ canEdit,
3082
+ isMobile,
3083
+ setTestOpen
3084
+ ]);
1202
3085
  const addBlock = useCallback((type, at) => {
1203
- const flow = valueRef.current;
1204
- if (type === "gatilho" && flow.nodes.some((n) => n.type === "gatilho")) {
3086
+ const cur = flowRef.current;
3087
+ if (isTrigger(type) && cur.nodes.some((n) => isTrigger(n.type))) {
1205
3088
  setNotice("Já existe um gatilho neste fluxo.");
1206
3089
  return;
1207
3090
  }
1208
- let pos = at;
1209
- if (!pos) {
3091
+ const size = {
3092
+ w: 336,
3093
+ h: estimateNodeHeight({
3094
+ type,
3095
+ data: { label: "" }
3096
+ })
3097
+ };
3098
+ let from = at;
3099
+ if (!from) {
1210
3100
  const rect = canvasRef.current?.getBoundingClientRect();
1211
3101
  const center = screenToFlowPosition({
1212
3102
  x: (rect?.left ?? 0) + (rect?.width ?? 0) / 2,
1213
3103
  y: (rect?.top ?? 0) + (rect?.height ?? 0) / 2
1214
3104
  });
1215
- pos = {
1216
- x: center.x - NODE_W / 2,
1217
- y: center.y - NODE_H / 2
1218
- };
1219
- while (flow.nodes.some((n) => Math.abs(n.position.x - pos.x) < 24 && Math.abs(n.position.y - pos.y) < 24)) pos = {
1220
- x: pos.x + 24,
1221
- y: pos.y + 24
3105
+ from = {
3106
+ x: center.x - size.w / 2,
3107
+ y: center.y - size.h / 2
1222
3108
  };
1223
3109
  }
1224
- emit(addNode(flow, type, pos, { reservedIds: everUsed.current }).flow);
1225
- }, [emit, screenToFlowPosition]);
1226
- /** Arrastar um item da paleta até o canvas, por pointer events (funciona com mouse e toque). */
3110
+ const measured = Object.fromEntries(rfNodes.flatMap((n) => n.measured?.width && n.measured.height ? [[n.id, {
3111
+ w: n.measured.width,
3112
+ h: n.measured.height
3113
+ }]] : []));
3114
+ const added = addNode(cur, type, findFreePosition(cur, size, from, measured), { reservedIds: everUsed.current });
3115
+ emit(added.flow);
3116
+ setConfigId(added.id);
3117
+ setView("config");
3118
+ setTestOpen(false);
3119
+ if (isMobile) setDrawerOpen(true);
3120
+ }, [
3121
+ emit,
3122
+ isMobile,
3123
+ rfNodes,
3124
+ screenToFlowPosition,
3125
+ setTestOpen
3126
+ ]);
3127
+ /** Arrastar um bloco da barra até o canvas, por pointer events (funciona com mouse e toque). */
1227
3128
  const startPaletteDrag = (e, type) => {
1228
3129
  if (e.pointerType === "mouse" && e.button !== 0) return;
1229
3130
  const origin = {
@@ -1263,8 +3164,11 @@ function FlowEditorInner({ value, onChange, readOnly = false, className }) {
1263
3164
  y: ev.clientY
1264
3165
  });
1265
3166
  addBlock(type, {
1266
- x: p.x - NODE_W / 2,
1267
- y: p.y - NODE_H / 2
3167
+ x: p.x - 168,
3168
+ y: p.y - estimateNodeHeight({
3169
+ type,
3170
+ data: { label: "" }
3171
+ }) / 2
1268
3172
  });
1269
3173
  }
1270
3174
  };
@@ -1283,14 +3187,6 @@ function FlowEditorInner({ value, onChange, readOnly = false, className }) {
1283
3187
  window.addEventListener("pointercancel", cancel);
1284
3188
  window.addEventListener("keydown", key);
1285
3189
  };
1286
- const openDrawer = (id) => {
1287
- if (!canEdit) return;
1288
- setDrawer((d) => ({
1289
- id,
1290
- session: (d?.session ?? 0) + 1
1291
- }));
1292
- setDrawerOpen(true);
1293
- };
1294
3190
  const viewBlock = (id) => {
1295
3191
  setRfNodes((nds) => nds.map((n) => n.selected === (n.id === id) ? n : {
1296
3192
  ...n,
@@ -1302,178 +3198,232 @@ function FlowEditorInner({ value, onChange, readOnly = false, className }) {
1302
3198
  maxZoom: 1
1303
3199
  });
1304
3200
  };
1305
- const startTest = () => setSim(startSimulation(value));
1306
- const closeTest = () => setSim(resetSimulation());
1307
- const deleteNode = deleting ? value.nodes.find((n) => n.id === deleting) : void 0;
1308
- return /* @__PURE__ */ jsxs("div", {
1309
- "data-rendra": "FLX-001",
1310
- className: cn("flow-editor flex min-w-0 flex-col gap-4", className),
1311
- children: [
1312
- /* @__PURE__ */ jsxs("div", {
1313
- className: "flex min-w-0 flex-col gap-4 lg:flex-row lg:items-start",
1314
- children: [/* @__PURE__ */ jsxs(Card, {
1315
- className: "min-w-0 flex-1",
1316
- children: [/* @__PURE__ */ jsxs(CardHeader, {
1317
- actions: /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
1318
- variant: "outline",
1319
- iconOnly: isMobile,
1320
- "aria-label": "Validar",
1321
- icon: /* @__PURE__ */ jsx(ListChecks, {}),
1322
- onClick: () => setValidated(true),
1323
- children: isMobile ? null : "Validar"
1324
- }), /* @__PURE__ */ jsx(Button, {
1325
- iconOnly: isMobile,
1326
- "aria-label": "Testar fluxo",
1327
- icon: /* @__PURE__ */ jsx(FlaskConical, {}),
1328
- onClick: startTest,
1329
- children: isMobile ? null : "Testar fluxo"
1330
- })] }),
1331
- children: [/* @__PURE__ */ jsx(CardTitle, { children: "Fluxo de automação" }), /* @__PURE__ */ jsx(CardDescription, { children: "Blocos, ligações e teste do caminho." })]
1332
- }), /* @__PURE__ */ jsxs(CardContent, {
1333
- className: "flex flex-col gap-4",
1334
- children: [
1335
- canBuild && /* @__PURE__ */ jsx("div", {
1336
- role: "group",
1337
- "aria-label": "Blocos",
1338
- className: "flex flex-wrap items-center gap-2",
1339
- children: FLOW_NODE_TYPES.map((type) => {
1340
- const Icon = FLOW_ICON[type];
1341
- return /* @__PURE__ */ jsx(Button, {
1342
- variant: "outline",
1343
- size: "sm",
1344
- icon: /* @__PURE__ */ jsx(Icon, {}),
1345
- className: "cursor-grab touch-none",
1346
- onPointerDown: (e) => startPaletteDrag(e, type),
1347
- onClick: () => {
1348
- if (suppressClick.current) return;
1349
- addBlock(type);
1350
- },
1351
- children: FLOW_TYPE_NAME[type]
1352
- }, type);
1353
- })
1354
- }),
1355
- /* @__PURE__ */ jsx("div", {
1356
- ref: canvasRef,
1357
- className: "min-w-0",
1358
- children: /* @__PURE__ */ jsx(FlowSurface, {
1359
- className: cn(drag?.over && "ring-2 ring-primary"),
1360
- nodes: rfNodes,
1361
- edges: rfEdges,
1362
- onNodesChange,
1363
- onEdgesChange,
1364
- onConnect,
1365
- nodesDraggable: canBuild,
1366
- nodesConnectable: canBuild,
1367
- elementsSelectable: canEdit,
1368
- deleteKeyCode: canEdit ? ["Delete", "Backspace"] : null,
1369
- onBeforeDelete: async () => canEdit && !drawerOpenRef.current,
1370
- onDelete: ({ nodes, edges }) => {
1371
- let flow = valueRef.current;
1372
- for (const e of edges) flow = removeEdge(flow, e.id);
1373
- for (const n of nodes) flow = removeNode(flow, n.id);
1374
- if (flow !== valueRef.current) emit(flow);
1375
- },
1376
- onNodeDragStop: (_, __, nodes) => commitMoves(nodes.map((n) => ({
1377
- id: n.id,
1378
- position: n.position
1379
- }))),
1380
- onNodeClick: (e, node) => {
1381
- const fromEditButton = e.target.closest("[data-flow-edit]");
1382
- if (isMobile || fromEditButton) openDrawer(node.id);
1383
- },
1384
- onNodeDoubleClick: (_, node) => openDrawer(node.id),
1385
- onKeyDown: (e) => {
1386
- const el = e.target;
1387
- if (e.key === "Enter" && el.classList?.contains("react-flow__node") && el.dataset.id) {
1388
- e.preventDefault();
1389
- openDrawer(el.dataset.id);
1390
- }
3201
+ useEffect(() => {
3202
+ if (testOpen && !blockStepRef.current) setSim((s) => s.status === "idle" ? startSimulation(flowRef.current) : s);
3203
+ if (testOpen && isMobile) setDrawerOpen(true);
3204
+ if (!testOpen) {
3205
+ setSim(resetSimulation());
3206
+ setBlockStep(null);
3207
+ }
3208
+ }, [testOpen, isMobile]);
3209
+ const openTest = () => {
3210
+ setTestOpen(true);
3211
+ showDrawer();
3212
+ };
3213
+ const playBlock = useCallback((id) => {
3214
+ setSim(resetSimulation());
3215
+ const step = simulateBlock(flowRef.current, id);
3216
+ setBlockStep(step);
3217
+ blockStepRef.current = step;
3218
+ setTestOpen(true);
3219
+ if (isMobile) setDrawerOpen(true);
3220
+ }, [isMobile, setTestOpen]);
3221
+ const focusBoard = () => canvasRef.current?.querySelector(".flow-surface")?.focus();
3222
+ const actions = useMemo(() => ({
3223
+ play: playBlock,
3224
+ ...canEdit ? {
3225
+ edit: openConfig,
3226
+ remove: (id) => setDeleting(id)
3227
+ } : {},
3228
+ ...canBuild ? { removeEdge: (id) => {
3229
+ emit(removeEdge(flowRef.current, id));
3230
+ focusBoard();
3231
+ } } : {}
3232
+ }), [
3233
+ canEdit,
3234
+ canBuild,
3235
+ playBlock,
3236
+ openConfig,
3237
+ emit
3238
+ ]);
3239
+ const closeSidebarPanel = () => {
3240
+ setView("blocks");
3241
+ setValidated(false);
3242
+ setDrawerOpen(false);
3243
+ };
3244
+ const panel = testOpen ? /* @__PURE__ */ jsx(FlowSimulationPanel, {
3245
+ flow,
3246
+ sim,
3247
+ blockStep,
3248
+ onSim: (s) => {
3249
+ setBlockStep(null);
3250
+ setSim(s);
3251
+ },
3252
+ onClose: isMobile ? void 0 : () => {
3253
+ setTestOpen(false);
3254
+ setDrawerOpen(false);
3255
+ },
3256
+ onViewBlock: viewBlock
3257
+ }) : view === "validate" ? /* @__PURE__ */ jsx(FlowValidationPanel, {
3258
+ flow,
3259
+ issues,
3260
+ onClose: isMobile ? void 0 : closeSidebarPanel,
3261
+ onViewBlock: viewBlock
3262
+ }) : view === "config" && configNode ? /* @__PURE__ */ jsx(BlockConfigPanel, {
3263
+ flow,
3264
+ nodeId: configNode.id,
3265
+ onApply: (next, message) => emit(next, message),
3266
+ onClose: isMobile ? void 0 : closeSidebarPanel
3267
+ }, configNode.id) : canEdit ? /* @__PURE__ */ jsx(FlowPanelShell, {
3268
+ title: "Adicionar bloco",
3269
+ children: /* @__PURE__ */ jsx(FlowBlockList, {
3270
+ onAdd: (type) => addBlock(type),
3271
+ onDragStart: canBuild ? startPaletteDrag : void 0,
3272
+ suppressClick
3273
+ })
3274
+ }) : null;
3275
+ const sidebarBody = panel && /* @__PURE__ */ jsx("div", {
3276
+ className: "nokey",
3277
+ children: panel
3278
+ });
3279
+ const showAside = !isMobile && sidebarBody !== null;
3280
+ return /* @__PURE__ */ jsx("div", {
3281
+ className: "@container min-w-0",
3282
+ children: /* @__PURE__ */ jsxs("div", {
3283
+ ref: rootRef,
3284
+ "data-rendra": "FLX-001",
3285
+ className: cn("flow-editor flex min-w-0 flex-col overflow-hidden rounded-surface border bg-card @5xl:h-board @5xl:min-h-viewer @5xl:flex-row", className),
3286
+ children: [
3287
+ /* @__PURE__ */ jsxs("div", {
3288
+ ref: canvasRef,
3289
+ className: cn("relative h-board min-w-0 md:h-viewer @5xl:h-full @5xl:flex-1", drag?.over && "ring-2 ring-primary ring-inset"),
3290
+ children: [
3291
+ /* @__PURE__ */ jsx(FlowActionsContext.Provider, {
3292
+ value: actions,
3293
+ children: /* @__PURE__ */ jsx(FlowSurface, {
3294
+ fill: false,
3295
+ nodes: rfNodes,
3296
+ edges: rfEdges,
3297
+ onNodesChange,
3298
+ onEdgesChange,
3299
+ onConnect,
3300
+ onConnectEnd,
3301
+ nodesDraggable: canBuild,
3302
+ nodesConnectable: canBuild,
3303
+ elementsSelectable: canEdit,
3304
+ deleteKeyCode: canEdit ? ["Delete", "Backspace"] : null,
3305
+ onBeforeDelete: async ({ nodes: gone }) => {
3306
+ if (!canEdit || drawerOpen || deletingRef.current) return false;
3307
+ if (gone.length === 0) return true;
3308
+ if (gone.length === 1) setDeleting(gone[0].id);
3309
+ else setNotice(NO_BLOCK);
3310
+ return false;
3311
+ },
3312
+ onDelete: ({ edges: gone }) => {
3313
+ let next = flowRef.current;
3314
+ for (const e of gone) next = removeEdge(next, e.id);
3315
+ if (next !== flowRef.current) emit(next);
3316
+ },
3317
+ onNodeDragStop: (_, __, nodes) => commitMoves(nodes.map((n) => ({
3318
+ id: n.id,
3319
+ position: n.position
3320
+ }))),
3321
+ onNodeClick: (e, node) => {
3322
+ if (e.target.closest("button")) return;
3323
+ openConfig(node.id);
3324
+ },
3325
+ onPaneClick: () => {
3326
+ if (view === "config" && !isMobile) setView("blocks");
3327
+ },
3328
+ onKeyDown: (e) => {
3329
+ const el = e.target;
3330
+ if (e.key === "Enter" && el.classList?.contains("react-flow__node") && el.dataset.id) {
3331
+ e.preventDefault();
3332
+ openConfig(el.dataset.id);
1391
3333
  }
1392
- })
1393
- }),
1394
- /* @__PURE__ */ jsx("div", {
1395
- role: "status",
1396
- "aria-live": "polite",
1397
- id: noticeId,
1398
- className: cn(!notice && "sr-only"),
1399
- children: notice && /* @__PURE__ */ jsx(Alert, {
1400
- type: "warning",
1401
- title: notice,
1402
- onDismiss: () => setNotice(null)
1403
- })
1404
- }),
1405
- validated && /* @__PURE__ */ jsx("section", {
1406
- "aria-label": "Validação do fluxo",
1407
- "aria-live": "polite",
1408
- className: "flex flex-col gap-3",
1409
- children: issues.length === 0 ? /* @__PURE__ */ jsx(Alert, {
1410
- type: "success",
1411
- title: "Fluxo válido"
1412
- }) : /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Alert, {
1413
- type: "error",
1414
- title: issues.length === 1 ? "1 problema encontrado" : `${issues.length} problemas encontrados`
1415
- }), /* @__PURE__ */ jsx("ul", {
1416
- className: "flex flex-col gap-2",
1417
- children: issues.map((i, k) => /* @__PURE__ */ jsxs("li", {
1418
- className: "flex flex-wrap items-center justify-between gap-2 rounded-control border bg-card px-3 py-2 text-sm",
1419
- children: [/* @__PURE__ */ jsx("span", { children: i.message }), i.nodeId && /* @__PURE__ */ jsx(Button, {
1420
- size: "sm",
1421
- variant: "outline",
1422
- "aria-label": `Ver bloco ${nodeName(value.nodes.find((n) => n.id === i.nodeId) ?? {
1423
- type: "acao",
1424
- data: { label: i.nodeId }
1425
- })}`,
1426
- onClick: () => viewBlock(i.nodeId),
1427
- children: "Ver bloco"
1428
- })]
1429
- }, `${i.code}-${i.nodeId ?? i.edgeId ?? k}`))
1430
- })] })
3334
+ }
1431
3335
  })
1432
- ]
1433
- })]
1434
- }), sim.status !== "idle" && /* @__PURE__ */ jsx(SimulationPanel, {
1435
- flow: value,
1436
- sim,
1437
- onSim: setSim,
1438
- onClose: closeTest
1439
- })]
1440
- }),
1441
- drag && /* @__PURE__ */ jsx("div", {
1442
- "aria-hidden": true,
1443
- "data-flow-drag": true,
1444
- className: "pointer-events-none fixed top-0 left-0 z-50 translate-x-(--flow-x) translate-y-(--flow-y) rounded-control border bg-card px-3 py-2 text-sm shadow-md",
1445
- style: {
1446
- "--flow-x": `${drag.x}px`,
1447
- "--flow-y": `${drag.y}px`
1448
- },
1449
- children: FLOW_TYPE_NAME[drag.type]
1450
- }),
1451
- canEdit && drawer && /* @__PURE__ */ jsx(BlockDrawer, {
1452
- flow: value,
1453
- nodeId: drawer.id,
1454
- open: drawerOpen,
1455
- onOpenChange: setDrawerOpen,
1456
- onApply: (flow) => {
1457
- setDrawerOpen(false);
1458
- emit(flow);
1459
- },
1460
- onDelete: setDeleting
1461
- }, drawer.session),
1462
- /* @__PURE__ */ jsx(Modal, {
1463
- open: Boolean(deleting),
1464
- onOpenChange: (o) => !o && setDeleting(null),
1465
- type: "destructive",
1466
- title: "Excluir este bloco?",
1467
- description: deleteNode ? `O bloco "${nodeName(deleteNode)}" e as ligações dele serão apagados.` : void 0,
1468
- confirmLabel: "Excluir",
1469
- onConfirm: () => {
1470
- if (!deleting) return;
1471
- emit(removeNode(valueRef.current, deleting));
1472
- setDeleting(null);
1473
- setDrawerOpen(false);
1474
- }
1475
- })
1476
- ]
3336
+ }),
3337
+ (toolbar || isMobile && canEdit) && /* @__PURE__ */ jsxs("div", {
3338
+ className: "absolute top-2 right-2 z-10 flex gap-2",
3339
+ children: [isMobile && canEdit && /* @__PURE__ */ jsx(Button, {
3340
+ variant: "outline",
3341
+ iconOnly: true,
3342
+ "aria-label": "Blocos",
3343
+ icon: /* @__PURE__ */ jsx(Plus, {}),
3344
+ onClick: openBlocks
3345
+ }), toolbar && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Button, {
3346
+ variant: "outline",
3347
+ iconOnly: isMobile,
3348
+ "aria-label": "Validar",
3349
+ icon: /* @__PURE__ */ jsx(ListChecks, {}),
3350
+ onClick: () => {
3351
+ setValidated(true);
3352
+ setView("validate");
3353
+ setTestOpen(false);
3354
+ showDrawer();
3355
+ },
3356
+ children: isMobile ? null : "Validar"
3357
+ }), /* @__PURE__ */ jsx(Button, {
3358
+ iconOnly: isMobile,
3359
+ "aria-label": "Testar fluxo",
3360
+ icon: /* @__PURE__ */ jsx(FlaskConical, {}),
3361
+ onClick: openTest,
3362
+ children: isMobile ? null : "Testar fluxo"
3363
+ })] })]
3364
+ }),
3365
+ /* @__PURE__ */ jsx("div", {
3366
+ role: "status",
3367
+ "aria-live": "polite",
3368
+ id: noticeId,
3369
+ "data-flow-notice": true,
3370
+ className: cn(notice ? "absolute inset-x-2 top-16 z-10" : "sr-only"),
3371
+ children: notice && /* @__PURE__ */ jsx(Alert, {
3372
+ type: "warning",
3373
+ title: notice,
3374
+ onDismiss: () => setNotice(null)
3375
+ })
3376
+ })
3377
+ ]
3378
+ }),
3379
+ showAside && /* @__PURE__ */ jsx("aside", {
3380
+ "aria-label": "Painel do fluxo",
3381
+ className: "w-full shrink-0 scrollbar-subtle border-t p-4 @5xl:h-full @5xl:w-flow-panel @5xl:overflow-y-auto @5xl:border-t-0 @5xl:border-l",
3382
+ children: /* @__PURE__ */ jsx("div", {
3383
+ role: "presentation",
3384
+ onKeyDown: (e) => {
3385
+ if (e.key === "Escape" && view === "config" && !testOpen && !e.target.closest("[role=\"combobox\"],[role=\"listbox\"],[role=\"dialog\"],[data-radix-popper-content-wrapper],[cmdk-root]")) setView("blocks");
3386
+ },
3387
+ children: sidebarBody
3388
+ })
3389
+ }),
3390
+ drag && /* @__PURE__ */ jsx("div", {
3391
+ "aria-hidden": true,
3392
+ "data-flow-drag": true,
3393
+ className: "pointer-events-none fixed top-0 left-0 z-50 translate-x-(--flow-x) translate-y-(--flow-y) rounded-control border bg-card px-3 py-2 text-sm shadow-md",
3394
+ style: {
3395
+ "--flow-x": `${drag.x}px`,
3396
+ "--flow-y": `${drag.y}px`
3397
+ },
3398
+ children: FLOW_TYPE_NAME[drag.type]
3399
+ }),
3400
+ isMobile && sidebarBody !== null && /* @__PURE__ */ jsx(Drawer, {
3401
+ open: drawerOpen,
3402
+ onOpenChange: (open) => {
3403
+ setDrawerOpen(open);
3404
+ if (!open) {
3405
+ if (testOpen) setTestOpen(false);
3406
+ setView("blocks");
3407
+ setValidated(false);
3408
+ }
3409
+ },
3410
+ title: "Fluxo de automação",
3411
+ children: sidebarBody
3412
+ }),
3413
+ canEdit && deleting && /* @__PURE__ */ jsx(DeleteBlockModal, {
3414
+ flow,
3415
+ nodeId: deleting,
3416
+ onClose: () => setDeleting(null),
3417
+ onConfirm: (mode) => {
3418
+ emit(removeNode(flowRef.current, deleting, mode));
3419
+ setDeleting(null);
3420
+ setConfigId((id) => id === deleting ? null : id);
3421
+ setView("blocks");
3422
+ setDrawerOpen(false);
3423
+ }
3424
+ }, deleting)
3425
+ ]
3426
+ })
1477
3427
  });
1478
3428
  }
1479
3429
  function FlowEditor(props) {