@rendra-ui/web 2.5.0 → 2.7.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 (68) hide show
  1. package/README.md +13 -3
  2. package/dist/base.css +1 -1
  3. package/dist/{empty-state-Cdb1vkMT.js → brand-feedback-icon-BYNcqBel.js} +4 -64
  4. package/dist/brand-feedback-icon-BYNcqBel.js.map +1 -0
  5. package/dist/{button-BzJgR2gH.js → button-DlzWwpZk.js} +7 -7
  6. package/dist/button-DlzWwpZk.js.map +1 -0
  7. package/dist/chart.js +15 -13
  8. package/dist/chart.js.map +1 -1
  9. package/dist/cli/auditar.js +1 -1
  10. package/dist/cli/codigos.js +2 -2
  11. package/dist/cli/executar-auditar.js +1 -1
  12. package/dist/cli/help.js +1 -1
  13. package/dist/cli/trocar.js +2 -2
  14. package/dist/cli/typescript-loader.js +1 -1
  15. package/dist/{cn-Cw7M5kEP.js → cn-B0N3Pek0.js} +2 -2
  16. package/dist/{cn-Cw7M5kEP.js.map → cn-B0N3Pek0.js.map} +1 -1
  17. package/dist/{components-DkpspH9h.js → components-CpQPMGfT.js} +18 -2
  18. package/dist/{components-DkpspH9h.js.map → components-CpQPMGfT.js.map} +1 -1
  19. package/dist/components.css +90 -1
  20. package/dist/document-viewer.js +4 -4
  21. package/dist/{tooltip-BAMSaGbS.js → dropdown-menu-M8vs0byD.js} +15 -81
  22. package/dist/dropdown-menu-M8vs0byD.js.map +1 -0
  23. package/dist/empty-state-a47Kd_Ci.js +67 -0
  24. package/dist/empty-state-a47Kd_Ci.js.map +1 -0
  25. package/dist/{features-max--5VEt6M7.js → features-max-ScxV8gb3.js} +1 -1
  26. package/dist/flow.js +1485 -0
  27. package/dist/flow.js.map +1 -0
  28. package/dist/index.js +130 -1899
  29. package/dist/index.js.map +1 -1
  30. package/dist/motion-context-BUMPf0e9.js +222 -0
  31. package/dist/motion-context-BUMPf0e9.js.map +1 -0
  32. package/dist/motion-on-CNN6y8g-.js +29 -0
  33. package/dist/motion-on-CNN6y8g-.js.map +1 -0
  34. package/dist/motion.js +3 -0
  35. package/dist/{rendra-provider-CRe0XdOU.js → rendra-provider-DEURIIL3.js} +3 -3
  36. package/dist/rendra-provider-DEURIIL3.js.map +1 -0
  37. package/dist/rich-text-editor.js +4 -3
  38. package/dist/rich-text-editor.js.map +1 -1
  39. package/dist/router-bridge.js +2 -2
  40. package/dist/router-bridge.js.map +1 -1
  41. package/dist/timeline-BOh7gUdA.js +1803 -0
  42. package/dist/timeline-BOh7gUdA.js.map +1 -0
  43. package/dist/tokens.css +1 -1
  44. package/dist/tooltip-Ym9S4h9_.js +76 -0
  45. package/dist/tooltip-Ym9S4h9_.js.map +1 -0
  46. package/dist/types/components/rendra-provider.d.ts +7 -2
  47. package/dist/types/components/rendra-router-bridge.d.ts +2 -1
  48. package/dist/types/components/ui/flow-editor.d.ts +11 -0
  49. package/dist/types/components/ui/flow-viewer.d.ts +58 -0
  50. package/dist/types/flow.d.ts +3 -0
  51. package/dist/types/index.d.ts +1 -0
  52. package/dist/types/lib/flow.d.ts +121 -0
  53. package/dist/types/lib/motion/index.d.ts +1 -1
  54. package/dist/types/lib/motion/motion-context.d.ts +21 -6
  55. package/dist/types/lib/motion/motion-on.d.ts +12 -0
  56. package/dist/types/motion.d.ts +1 -0
  57. package/dist/{use-breakpoint-O9eqFmsF.js → use-breakpoint-En17oXKY.js} +2 -2
  58. package/dist/{use-breakpoint-O9eqFmsF.js.map → use-breakpoint-En17oXKY.js.map} +1 -1
  59. package/dist/{use-motion-presets-BrkgL8lG.js → use-motion-presets-D3ww1oJX.js} +3 -3
  60. package/dist/{use-motion-presets-BrkgL8lG.js.map → use-motion-presets-D3ww1oJX.js.map} +1 -1
  61. package/dist/widget-grid.js +2 -2
  62. package/package.json +14 -1
  63. package/dist/button-BzJgR2gH.js.map +0 -1
  64. package/dist/empty-state-Cdb1vkMT.js.map +0 -1
  65. package/dist/motion-context-5BRIQAJm.js +0 -107
  66. package/dist/motion-context-5BRIQAJm.js.map +0 -1
  67. package/dist/rendra-provider-CRe0XdOU.js.map +0 -1
  68. package/dist/tooltip-BAMSaGbS.js.map +0 -1
package/dist/flow.js ADDED
@@ -0,0 +1,1485 @@
1
+ /*! Rendra Design System v2.7.0 | MIT | © 2026 Bruno Magalhaes | brunomagalhaes.me */
2
+ import { t as cn } from "./cn-B0N3Pek0.js";
3
+ import { i as useMotionLib } from "./motion-context-BUMPf0e9.js";
4
+ import { t as useMotionPresets } from "./use-motion-presets-D3ww1oJX.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-BOh7gUdA.js";
6
+ import { t as Button } from "./button-DlzWwpZk.js";
7
+ import { n as useBreakpoint } from "./use-breakpoint-En17oXKY.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";
10
+ 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 "@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
+ ];
21
+ var FLOW_WAIT_UNITS = [
22
+ "segundos",
23
+ "minutos",
24
+ "horas",
25
+ "dias"
26
+ ];
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
+ var WAIT_SINGULAR = {
40
+ segundos: "segundo",
41
+ minutos: "minuto",
42
+ horas: "hora",
43
+ dias: "dia"
44
+ };
45
+ var DEFAULT_WAIT = {
46
+ duration: 5,
47
+ unit: "minutos"
48
+ };
49
+ var isUnit = (v) => typeof v === "string" && FLOW_WAIT_UNITS.includes(v);
50
+ /** A duração do bloco de espera ("5 minutos"), ou `null` se o config não a define. */
51
+ function formatWait(config) {
52
+ const duration = config?.duration;
53
+ const unit = config?.unit;
54
+ if (typeof duration !== "number" || !Number.isFinite(duration) || !isUnit(unit)) return null;
55
+ return `${duration} ${duration === 1 ? WAIT_SINGULAR[unit] : unit}`;
56
+ }
57
+ /** Texto da espera simulada: nunca espera de verdade. */
58
+ function describeWait(config) {
59
+ const wait = formatWait(config);
60
+ return wait ? `Aguardaria ${wait}` : "Aguardaria o tempo definido no bloco";
61
+ }
62
+ /** Nome do bloco para mensagens e rótulos: o rótulo, ou o tipo quando ele está vazio. */
63
+ var nodeName = (n) => n.data.label.trim() || FLOW_TYPE_NAME[n.type];
64
+ /** Ligações que saem de cada bloco, na ordem do fluxo. */
65
+ function outgoing(flow) {
66
+ const map = /* @__PURE__ */ new Map();
67
+ for (const e of flow.edges) map.set(e.source, [...map.get(e.source) ?? [], e]);
68
+ return map;
69
+ }
70
+ /** Problemas do fluxo, em ordem estável: a mesma entrada dá sempre a mesma lista. */
71
+ function validateFlow(flow) {
72
+ const issues = [];
73
+ const byId = new Map(flow.nodes.map((n) => [n.id, n]));
74
+ const out = outgoing(flow);
75
+ const triggers = flow.nodes.filter((n) => n.type === "gatilho");
76
+ if (triggers.length === 0) issues.push({
77
+ code: "sem-gatilho",
78
+ message: "O fluxo precisa de um bloco Gatilho para começar."
79
+ });
80
+ else if (triggers.length > 1) issues.push({
81
+ code: "varios-gatilhos",
82
+ message: `O fluxo tem ${triggers.length} gatilhos, mas deve ter só um.`
83
+ });
84
+ const sound = [];
85
+ for (const e of flow.edges) {
86
+ const from = byId.get(e.source);
87
+ const to = byId.get(e.target);
88
+ if (!from || !to) {
89
+ issues.push({
90
+ code: "ligacao-invalida",
91
+ message: "Há uma ligação que aponta para um bloco que não existe.",
92
+ edgeId: e.id
93
+ });
94
+ continue;
95
+ }
96
+ if (from.type === "condicao" ? !e.branch : e.branch) {
97
+ issues.push({
98
+ 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.`,
100
+ nodeId: from.id,
101
+ edgeId: e.id
102
+ });
103
+ continue;
104
+ }
105
+ sound.push(e);
106
+ }
107
+ for (const n of flow.nodes) {
108
+ const edges = out.get(n.id) ?? [];
109
+ 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".`,
122
+ nodeId: n.id
123
+ });
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
128
+ });
129
+ if (sim > 1 || nao > 1) issues.push({
130
+ code: "condicao-saida-repetida",
131
+ message: `A condição "${name}" tem mais de uma saída para o mesmo ramo.`,
132
+ nodeId: n.id
133
+ });
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({
140
+ code: "varias-saidas",
141
+ message: `O bloco "${name}" tem mais de uma saída; só a condição pode ter.`,
142
+ nodeId: n.id
143
+ });
144
+ }
145
+ const next = /* @__PURE__ */ new Map();
146
+ for (const e of sound) next.set(e.source, [...next.get(e.source) ?? [], e.target]);
147
+ if (triggers.length > 0) {
148
+ const seen = /* @__PURE__ */ new Set();
149
+ const stack = triggers.map((t) => t.id);
150
+ while (stack.length) {
151
+ const id = stack.pop();
152
+ if (seen.has(id)) continue;
153
+ seen.add(id);
154
+ stack.push(...next.get(id) ?? []);
155
+ }
156
+ for (const n of flow.nodes) if (!seen.has(n.id)) issues.push({
157
+ code: "inalcancavel",
158
+ message: `O bloco "${nodeName(n)}" não é alcançado a partir do gatilho.`,
159
+ nodeId: n.id
160
+ });
161
+ }
162
+ const state = /* @__PURE__ */ new Map();
163
+ const path = [];
164
+ const visit = (id) => {
165
+ state.set(id, 1);
166
+ path.push(id);
167
+ for (const t of next.get(id) ?? []) if (state.get(t) === 1) {
168
+ const loop = [...path.slice(path.indexOf(t)), t].map((x) => `"${nodeName(byId.get(x))}"`);
169
+ issues.push({
170
+ code: "ciclo",
171
+ message: `O fluxo volta para um bloco por onde já passou: ${loop.join(" → ")}.`,
172
+ nodeId: t
173
+ });
174
+ } else if (!state.has(t)) visit(t);
175
+ path.pop();
176
+ state.set(id, 2);
177
+ };
178
+ for (const n of flow.nodes) if (!state.has(n.id)) visit(n.id);
179
+ return issues;
180
+ }
181
+ var sortKeys = (v) => {
182
+ if (Array.isArray(v)) return v.map(sortKeys);
183
+ if (v && typeof v === "object") return Object.fromEntries(Object.keys(v).sort().map((k) => [k, sortKeys(v[k])]));
184
+ return v;
185
+ };
186
+ /** JSON do fluxo, com a ordem das chaves fixa: o mesmo fluxo dá sempre o mesmo texto. */
187
+ function serializeFlow(flow) {
188
+ const nodes = flow.nodes.map((n) => ({
189
+ id: n.id,
190
+ type: n.type,
191
+ position: {
192
+ x: n.position.x,
193
+ y: n.position.y
194
+ },
195
+ data: n.data.config ? {
196
+ label: n.data.label,
197
+ config: sortKeys(n.data.config)
198
+ } : { label: n.data.label }
199
+ }));
200
+ const edges = flow.edges.map((e) => e.branch ? {
201
+ id: e.id,
202
+ source: e.source,
203
+ target: e.target,
204
+ branch: e.branch
205
+ } : {
206
+ id: e.id,
207
+ source: e.source,
208
+ target: e.target
209
+ });
210
+ return JSON.stringify({
211
+ nodes,
212
+ edges
213
+ }, null, 2);
214
+ }
215
+ 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. */
217
+ function parseFlow(text) {
218
+ let raw;
219
+ try {
220
+ raw = JSON.parse(text);
221
+ } catch {
222
+ return {
223
+ ok: false,
224
+ errors: ["O texto não é um JSON válido."]
225
+ };
226
+ }
227
+ if (!isObject(raw)) return {
228
+ ok: false,
229
+ errors: ["O JSON do fluxo precisa ser um objeto."]
230
+ };
231
+ if (!Array.isArray(raw.nodes) || !Array.isArray(raw.edges)) return {
232
+ ok: false,
233
+ errors: [`O campo "${!Array.isArray(raw.nodes) ? "nodes" : "edges"}" precisa ser uma lista.`]
234
+ };
235
+ const errors = [];
236
+ const nodes = [];
237
+ const ids = /* @__PURE__ */ new Set();
238
+ raw.nodes.forEach((item, i) => {
239
+ const at = `Bloco ${i + 1}`;
240
+ if (!isObject(item)) return void errors.push(`${at}: precisa ser um objeto.`);
241
+ const { id, type, position, data } = item;
242
+ let ok = true;
243
+ if (typeof id !== "string" || id === "") {
244
+ errors.push(`${at}: falta o "id".`);
245
+ ok = false;
246
+ } else if (ids.has(id)) {
247
+ errors.push(`${at}: o id "${id}" está repetido.`);
248
+ ok = false;
249
+ } else ids.add(id);
250
+ if (!FLOW_NODE_TYPES.includes(type)) {
251
+ errors.push(`${at}: tipo desconhecido (${String(type)}).`);
252
+ ok = false;
253
+ }
254
+ const pos = isObject(position) ? position : null;
255
+ if (!pos || typeof pos.x !== "number" || typeof pos.y !== "number" || !Number.isFinite(pos.x) || !Number.isFinite(pos.y)) {
256
+ errors.push(`${at}: "position" precisa ter "x" e "y" numéricos.`);
257
+ ok = false;
258
+ }
259
+ if (!isObject(data) || typeof data.label !== "string") {
260
+ errors.push(`${at}: "data.label" precisa ser um texto.`);
261
+ ok = false;
262
+ } else if (data.config !== void 0 && !isObject(data.config)) {
263
+ errors.push(`${at}: "data.config" precisa ser um objeto.`);
264
+ ok = false;
265
+ }
266
+ if (ok) {
267
+ const d = data;
268
+ nodes.push({
269
+ id,
270
+ type,
271
+ position: {
272
+ x: pos.x,
273
+ y: pos.y
274
+ },
275
+ data: d.config ? {
276
+ label: d.label,
277
+ config: d.config
278
+ } : { label: d.label }
279
+ });
280
+ }
281
+ });
282
+ const edges = [];
283
+ const edgeIds = /* @__PURE__ */ new Set();
284
+ raw.edges.forEach((item, i) => {
285
+ const at = `Ligação ${i + 1}`;
286
+ if (!isObject(item)) return void errors.push(`${at}: precisa ser um objeto.`);
287
+ const { id, source, target, branch } = item;
288
+ let ok = true;
289
+ for (const [campo, v] of [
290
+ ["id", id],
291
+ ["source", source],
292
+ ["target", target]
293
+ ]) if (typeof v !== "string" || v === "") {
294
+ errors.push(`${at}: falta o campo "${campo}".`);
295
+ ok = false;
296
+ }
297
+ if (ok && edgeIds.has(id)) {
298
+ errors.push(`${at}: o id "${id}" está repetido.`);
299
+ ok = false;
300
+ }
301
+ if (branch !== void 0 && branch !== "sim" && branch !== "nao") {
302
+ errors.push(`${at}: "branch" só pode ser "sim" ou "nao".`);
303
+ ok = false;
304
+ }
305
+ if (ok) {
306
+ edgeIds.add(id);
307
+ edges.push(branch ? {
308
+ id,
309
+ source,
310
+ target,
311
+ branch
312
+ } : {
313
+ id,
314
+ source,
315
+ target
316
+ });
317
+ }
318
+ });
319
+ return errors.length > 0 ? {
320
+ ok: false,
321
+ errors
322
+ } : {
323
+ ok: true,
324
+ flow: {
325
+ nodes,
326
+ edges
327
+ }
328
+ };
329
+ }
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 = {
337
+ id,
338
+ 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
+ };
363
+ }
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
+ }));
612
+ }
613
+ function FlowBlock({ id, data, selected, isConnectable }) {
614
+ const lib = useMotionLib();
615
+ const presets = useMotionPresets();
616
+ const uid = useId();
617
+ const Icon = FLOW_ICON[data.kind];
618
+ const wait = data.kind === "espera" ? formatWait(data.config) : null;
619
+ const invalid = data.issues.length > 0;
620
+ const issueId = `${uid}-problema`;
621
+ const handle = cn("rounded-full border-2 border-card", data.handles === "lg" ? "size-6" : "size-2");
622
+ const ring = "pointer-events-none absolute -inset-1 rounded-block border-2 border-primary";
623
+ return /* @__PURE__ */ jsxs("div", {
624
+ "data-flow-node": true,
625
+ "data-kind": data.kind,
626
+ "data-block-id": id,
627
+ "data-active": data.active ? "true" : void 0,
628
+ "data-invalid": invalid ? "true" : void 0,
629
+ "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"),
632
+ children: [
633
+ data.active && (lib ? /* @__PURE__ */ jsx(lib.m.span, {
634
+ "aria-hidden": true,
635
+ "data-flow-pulse": true,
636
+ className: ring,
637
+ animate: presets.reduced ? { opacity: 1 } : { opacity: [
638
+ 1,
639
+ .3,
640
+ 1
641
+ ] },
642
+ transition: presets.reduced ? presets.fade.transition : {
643
+ duration: presets.tokens.duration.pulse,
644
+ ease: presets.tokens.ease.inOut,
645
+ repeat: Infinity
646
+ }
647
+ }) : /* @__PURE__ */ jsx("span", {
648
+ "aria-hidden": true,
649
+ className: ring
650
+ })),
651
+ data.kind !== "gatilho" && /* @__PURE__ */ jsx(Handle, {
652
+ type: "target",
653
+ position: Position.Top,
654
+ className: handle,
655
+ isConnectable
656
+ }),
657
+ /* @__PURE__ */ jsxs("div", {
658
+ className: "flex items-start gap-3",
659
+ children: [
660
+ /* @__PURE__ */ jsx("span", {
661
+ className: cn("flex size-8 shrink-0 items-center justify-center rounded-block", TONE[data.kind]),
662
+ children: /* @__PURE__ */ jsx(Icon, {
663
+ className: "size-icon-md",
664
+ "aria-hidden": true
665
+ })
666
+ }),
667
+ /* @__PURE__ */ jsxs("div", {
668
+ className: "flex min-w-0 flex-1 flex-col",
669
+ 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
677
+ }),
678
+ wait && /* @__PURE__ */ jsx("span", {
679
+ className: "text-xs text-muted-foreground",
680
+ children: wait
681
+ }),
682
+ data.active && /* @__PURE__ */ jsx("span", {
683
+ className: "text-xs font-medium text-primary-text",
684
+ children: "Ativo"
685
+ })
686
+ ]
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
+ })
700
+ ]
701
+ }),
702
+ invalid && /* @__PURE__ */ jsxs("p", {
703
+ 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",
705
+ children: [
706
+ /* @__PURE__ */ jsx(TriangleAlert, {
707
+ className: "size-icon-sm shrink-0",
708
+ "aria-hidden": true
709
+ }),
710
+ /* @__PURE__ */ jsx("strong", { children: "Problema" }),
711
+ " ",
712
+ data.issues.join(" ")
713
+ ]
714
+ }),
715
+ data.kind === "condicao" && /* @__PURE__ */ jsxs(Fragment$1, { children: [
716
+ /* @__PURE__ */ jsx("span", {
717
+ className: "absolute inset-y-0 right-4 flex items-center text-xs font-medium",
718
+ children: "Sim"
719
+ }),
720
+ /* @__PURE__ */ jsx("span", {
721
+ className: "absolute inset-x-0 bottom-3 text-center text-xs font-medium",
722
+ 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
+ })
738
+ ] }),
739
+ data.kind !== "condicao" && data.kind !== "fim" && /* @__PURE__ */ jsx(Handle, {
740
+ type: "source",
741
+ position: Position.Bottom,
742
+ className: handle,
743
+ isConnectable
744
+ })
745
+ ]
746
+ });
747
+ }
748
+ var nodeTypes = { bloco: FlowBlock };
749
+ var defaultEdgeOptions = {
750
+ type: "smoothstep",
751
+ markerEnd: { type: MarkerType.ArrowClosed }
752
+ };
753
+ var fitViewOptions = {
754
+ padding: {
755
+ top: "64px",
756
+ right: "16px",
757
+ bottom: "64px",
758
+ left: "16px"
759
+ },
760
+ maxZoom: 1
761
+ };
762
+ /** Aproximar, afastar e enquadrar, com 44 px de toque (os botões da biblioteca têm 26 px). */
763
+ function FlowZoomControls() {
764
+ const { zoomIn, zoomOut, fitView } = useReactFlow();
765
+ const cls = "min-h-touch min-w-touch";
766
+ return /* @__PURE__ */ jsxs("div", {
767
+ className: "absolute top-2 left-2 z-10 flex gap-2",
768
+ children: [
769
+ /* @__PURE__ */ jsx(Button, {
770
+ variant: "outline",
771
+ size: "sm",
772
+ iconOnly: true,
773
+ "aria-label": "Aproximar",
774
+ className: cls,
775
+ icon: /* @__PURE__ */ jsx(ZoomIn, {}),
776
+ onClick: () => void zoomIn()
777
+ }),
778
+ /* @__PURE__ */ jsx(Button, {
779
+ variant: "outline",
780
+ size: "sm",
781
+ iconOnly: true,
782
+ "aria-label": "Afastar",
783
+ className: cls,
784
+ icon: /* @__PURE__ */ jsx(ZoomOut, {}),
785
+ onClick: () => void zoomOut()
786
+ }),
787
+ /* @__PURE__ */ jsx(Button, {
788
+ variant: "outline",
789
+ size: "sm",
790
+ iconOnly: true,
791
+ "aria-label": "Enquadrar",
792
+ className: cls,
793
+ icon: /* @__PURE__ */ jsx(Maximize2, {}),
794
+ onClick: () => void fitView(fitViewOptions)
795
+ })
796
+ ]
797
+ });
798
+ }
799
+ /** Enquadra de novo quando a largura do canvas muda (painel do teste abrindo, janela redimensionada). */
800
+ function FlowAutoFit() {
801
+ const { fitView } = useReactFlow();
802
+ const width = useStore((st) => st.width);
803
+ const before = useRef(0);
804
+ useEffect(() => {
805
+ if (before.current > 0 && width > 0 && width !== before.current) fitView(fitViewOptions);
806
+ if (width > 0) before.current = width;
807
+ }, [width, fitView]);
808
+ return null;
809
+ }
810
+ /**
811
+ * O canvas: contêiner com a altura que sobra na tela (`h-board`), tema pelos tokens e a
812
+ * biblioteca por dentro. A rolagem horizontal é do canvas (`data-allow-overflow`), nunca da página.
813
+ */
814
+ function FlowSurface({ className, children, ...props }) {
815
+ const ref = useFillHeight();
816
+ return /* @__PURE__ */ jsx("div", {
817
+ ref,
818
+ "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),
820
+ style: {
821
+ "--xy-edge-stroke": "var(--rendra-muted-foreground)",
822
+ "--xy-edge-stroke-selected": "var(--rendra-primary)",
823
+ "--xy-edge-stroke-width": "2",
824
+ "--xy-connectionline-stroke": "var(--rendra-primary)",
825
+ "--xy-connectionline-stroke-width": "2",
826
+ "--xy-handle-background-color": "var(--rendra-primary)",
827
+ "--xy-attribution-background-color": "var(--rendra-card)",
828
+ "--xy-background-color": "var(--rendra-muted)"
829
+ },
830
+ children: /* @__PURE__ */ jsxs(ReactFlow, {
831
+ className: "flow-canvas",
832
+ nodeTypes,
833
+ defaultEdgeOptions,
834
+ defaultMarkerColor: "var(--rendra-muted-foreground)",
835
+ connectionLineType: ConnectionLineType.SmoothStep,
836
+ ariaLabelConfig: FLOW_ARIA,
837
+ fitView: true,
838
+ fitViewOptions,
839
+ minZoom: .25,
840
+ maxZoom: 2,
841
+ zoomOnDoubleClick: false,
842
+ zoomOnScroll: false,
843
+ preventScrolling: false,
844
+ ...props,
845
+ children: [
846
+ /* @__PURE__ */ jsx(FlowZoomControls, {}),
847
+ /* @__PURE__ */ jsx(FlowAutoFit, {}),
848
+ children
849
+ ]
850
+ })
851
+ });
852
+ }
853
+ /**
854
+ * Fluxo de automação só para ler: aproximar, afastar e mover o canvas. Não arrasta, não liga,
855
+ * não apaga. Para montar e editar, use o FlowEditor.
856
+ */
857
+ function FlowViewer({ value, activeId = null, issues, className }) {
858
+ const nodes = useMemo(() => toRfNodes(value, {
859
+ activeId,
860
+ issues,
861
+ handles: "sm",
862
+ edit: false
863
+ }), [
864
+ value,
865
+ activeId,
866
+ issues
867
+ ]);
868
+ const edges = useMemo(() => toRfEdges(value), [value]);
869
+ return /* @__PURE__ */ jsx("div", {
870
+ "data-rendra": "FLX-002",
871
+ className: cn("flow-viewer min-w-0", className),
872
+ children: /* @__PURE__ */ jsx(FlowSurface, {
873
+ nodes,
874
+ edges,
875
+ nodesDraggable: false,
876
+ nodesConnectable: false,
877
+ elementsSelectable: false,
878
+ edgesFocusable: false,
879
+ deleteKeyCode: null
880
+ })
881
+ });
882
+ }
883
+ //#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
+ };
915
+ });
916
+ }
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";
921
+ 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
928
+ };
929
+ }
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
943
+ });
944
+ const options = [{
945
+ value: NONE,
946
+ label: "Nenhum"
947
+ }, ...nextCandidates(flow, node.id).map((n) => ({
948
+ value: n.id,
949
+ label: `${FLOW_TYPE_NAME[n.type]}: ${nodeName(n)}`
950
+ }))];
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 })
974
+ })
975
+ });
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: [
1002
+ /* @__PURE__ */ jsx(Field, {
1003
+ label: "Rótulo",
1004
+ span: "full",
1005
+ error: draft.label.trim() === "" ? "Informe um rótulo." : void 0,
1006
+ children: /* @__PURE__ */ jsx(Input, {
1007
+ value: draft.label,
1008
+ onChange: (v) => set({ label: v })
1009
+ })
1010
+ }),
1011
+ node.type === "espera" && /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx(Field, {
1012
+ label: "Duração",
1013
+ span: "full",
1014
+ error: durationError,
1015
+ children: /* @__PURE__ */ jsx(Input, {
1016
+ value: draft.duration,
1017
+ inputMode: "numeric",
1018
+ onChange: (v) => set({ duration: v })
1019
+ })
1020
+ }), /* @__PURE__ */ jsx(Field, {
1021
+ 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
+ ]
1036
+ })
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) => ({
1052
+ id: `${i}-${s.nodeId}`,
1053
+ title: s.label,
1054
+ description: s.detail,
1055
+ date: `Passo ${i + 1}`,
1056
+ status: s.status
1057
+ }));
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
1071
+ }),
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",
1075
+ 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
+ }
1086
+ }),
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
+ })]
1096
+ }),
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
+ })]
1116
+ }),
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"]
1124
+ }),
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."
1129
+ })
1130
+ ]
1131
+ })]
1132
+ });
1133
+ }
1134
+ function FlowEditorInner({ value, onChange, readOnly = false, className }) {
1135
+ const { isMobile } = useBreakpoint();
1136
+ const canEdit = !readOnly;
1137
+ const canBuild = canEdit && !isMobile;
1138
+ const { screenToFlowPosition, fitView } = useReactFlow();
1139
+ const canvasRef = useRef(null);
1140
+ const noticeId = useId();
1141
+ const valueRef = useRef(value);
1142
+ valueRef.current = value;
1143
+ const everUsed = useRef(/* @__PURE__ */ new Set());
1144
+ value.nodes.forEach((n) => everUsed.current.add(n.id));
1145
+ const [validated, setValidated] = useState(false);
1146
+ const [sim, setSim] = useState(resetSimulation);
1147
+ const [notice, setNotice] = useState(null);
1148
+ const [drawer, setDrawer] = useState(null);
1149
+ const [drawerOpen, setDrawerOpen] = useState(false);
1150
+ const drawerOpenRef = useRef(false);
1151
+ drawerOpenRef.current = drawerOpen;
1152
+ const [deleting, setDeleting] = useState(null);
1153
+ const [drag, setDrag] = useState(null);
1154
+ const suppressClick = useRef(false);
1155
+ const issues = useMemo(() => validateFlow(value), [value]);
1156
+ 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, {
1159
+ activeId,
1160
+ issues: shownIssues,
1161
+ handles: canBuild ? "lg" : "sm",
1162
+ edit: canBuild
1163
+ }), [
1164
+ value,
1165
+ activeId,
1166
+ shownIssues,
1167
+ canBuild
1168
+ ]);
1169
+ const nextEdges = useMemo(() => toRfEdges(value), [value]);
1170
+ const [rfNodes, setRfNodes] = useState(nextNodes);
1171
+ const [rfEdges, setRfEdges] = useState(nextEdges);
1172
+ useEffect(() => setRfNodes((prev) => mergeNodes(prev, nextNodes)), [nextNodes]);
1173
+ useEffect(() => setRfEdges((prev) => mergeEdges(prev, nextEdges)), [nextEdges]);
1174
+ const emit = useCallback((flow) => {
1175
+ setNotice(null);
1176
+ onChange(flow);
1177
+ }, [onChange]);
1178
+ const commitMoves = useCallback((moves) => {
1179
+ let flow = valueRef.current;
1180
+ 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);
1183
+ }
1184
+ if (flow !== valueRef.current) emit(flow);
1185
+ }, [emit]);
1186
+ const onNodesChange = useCallback((changes) => {
1187
+ setRfNodes((nds) => applyNodeChanges(changes.filter((c) => c.type !== "remove"), nds));
1188
+ const moves = changes.flatMap((c) => c.type === "position" && c.dragging === false && c.position ? [{
1189
+ id: c.id,
1190
+ position: c.position
1191
+ }] : []);
1192
+ if (moves.length > 0 && canBuild) commitMoves(moves);
1193
+ }, [canBuild, commitMoves]);
1194
+ 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);
1201
+ }, [emit]);
1202
+ const addBlock = useCallback((type, at) => {
1203
+ const flow = valueRef.current;
1204
+ if (type === "gatilho" && flow.nodes.some((n) => n.type === "gatilho")) {
1205
+ setNotice("Já existe um gatilho neste fluxo.");
1206
+ return;
1207
+ }
1208
+ let pos = at;
1209
+ if (!pos) {
1210
+ const rect = canvasRef.current?.getBoundingClientRect();
1211
+ const center = screenToFlowPosition({
1212
+ x: (rect?.left ?? 0) + (rect?.width ?? 0) / 2,
1213
+ y: (rect?.top ?? 0) + (rect?.height ?? 0) / 2
1214
+ });
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
1222
+ };
1223
+ }
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). */
1227
+ const startPaletteDrag = (e, type) => {
1228
+ if (e.pointerType === "mouse" && e.button !== 0) return;
1229
+ const origin = {
1230
+ x: e.clientX,
1231
+ y: e.clientY
1232
+ };
1233
+ let moved = false;
1234
+ const inside = (x, y) => {
1235
+ const r = canvasRef.current?.getBoundingClientRect();
1236
+ return !!r && x >= r.left && x <= r.right && y >= r.top && y <= r.bottom;
1237
+ };
1238
+ const stop = () => {
1239
+ window.removeEventListener("pointermove", move);
1240
+ window.removeEventListener("pointerup", up);
1241
+ window.removeEventListener("pointercancel", cancel);
1242
+ window.removeEventListener("keydown", key);
1243
+ setDrag(null);
1244
+ };
1245
+ const move = (ev) => {
1246
+ if (!moved && Math.hypot(ev.clientX - origin.x, ev.clientY - origin.y) < DRAG_START) return;
1247
+ moved = true;
1248
+ setDrag({
1249
+ type,
1250
+ x: ev.clientX,
1251
+ y: ev.clientY,
1252
+ over: inside(ev.clientX, ev.clientY)
1253
+ });
1254
+ };
1255
+ const up = (ev) => {
1256
+ stop();
1257
+ if (!moved) return;
1258
+ suppressClick.current = true;
1259
+ setTimeout(() => suppressClick.current = false, 0);
1260
+ if (inside(ev.clientX, ev.clientY)) {
1261
+ const p = screenToFlowPosition({
1262
+ x: ev.clientX,
1263
+ y: ev.clientY
1264
+ });
1265
+ addBlock(type, {
1266
+ x: p.x - NODE_W / 2,
1267
+ y: p.y - NODE_H / 2
1268
+ });
1269
+ }
1270
+ };
1271
+ const cancel = () => {
1272
+ stop();
1273
+ if (moved) {
1274
+ suppressClick.current = true;
1275
+ setTimeout(() => suppressClick.current = false, 0);
1276
+ }
1277
+ };
1278
+ const key = (ev) => {
1279
+ if (ev.key === "Escape") cancel();
1280
+ };
1281
+ window.addEventListener("pointermove", move);
1282
+ window.addEventListener("pointerup", up);
1283
+ window.addEventListener("pointercancel", cancel);
1284
+ window.addEventListener("keydown", key);
1285
+ };
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
+ const viewBlock = (id) => {
1295
+ setRfNodes((nds) => nds.map((n) => n.selected === (n.id === id) ? n : {
1296
+ ...n,
1297
+ selected: n.id === id
1298
+ }));
1299
+ fitView({
1300
+ nodes: [{ id }],
1301
+ padding: .5,
1302
+ maxZoom: 1
1303
+ });
1304
+ };
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
+ }
1391
+ }
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
+ })] })
1431
+ })
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
+ ]
1477
+ });
1478
+ }
1479
+ function FlowEditor(props) {
1480
+ return /* @__PURE__ */ jsx(ReactFlowProvider, { children: /* @__PURE__ */ jsx(FlowEditorInner, { ...props }) });
1481
+ }
1482
+ //#endregion
1483
+ export { FlowEditor, FlowViewer, parseFlow, serializeFlow, validateFlow };
1484
+
1485
+ //# sourceMappingURL=flow.js.map