@caribeng/ui 22.4.0 → 22.5.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 (186) hide show
  1. package/README.md +311 -313
  2. package/fesm2022/caribeng-ui-account-picker.mjs +533 -0
  3. package/fesm2022/caribeng-ui-aging.mjs +488 -0
  4. package/fesm2022/{caribeng-ui-caribe-alert.mjs → caribeng-ui-alert.mjs} +45 -45
  5. package/fesm2022/caribeng-ui-amount.mjs +249 -0
  6. package/fesm2022/caribeng-ui-anchor.mjs +2 -2
  7. package/fesm2022/{caribeng-ui-caribe-animation-lottie.mjs → caribeng-ui-animation-lottie.mjs} +41 -41
  8. package/fesm2022/caribeng-ui-audit-log.mjs +323 -0
  9. package/fesm2022/caribeng-ui-avatar.mjs +2 -2
  10. package/fesm2022/caribeng-ui-breadcrumb.mjs +13 -3
  11. package/fesm2022/{caribeng-ui-caribe-button-group.mjs → caribeng-ui-button-group.mjs} +15 -15
  12. package/fesm2022/caribeng-ui-button.mjs +2 -2
  13. package/fesm2022/{caribeng-ui-caribe-card-content.mjs → caribeng-ui-card-content.mjs} +39 -39
  14. package/fesm2022/{caribeng-ui-caribe-card-move.mjs → caribeng-ui-card-move.mjs} +65 -65
  15. package/fesm2022/{caribeng-ui-caribe-card-primary.mjs → caribeng-ui-card-primary.mjs} +31 -31
  16. package/fesm2022/{caribeng-ui-caribe-card-select.mjs → caribeng-ui-card-select.mjs} +67 -67
  17. package/fesm2022/caribeng-ui-carousel.mjs +2 -2
  18. package/fesm2022/caribeng-ui-cascadeselect.mjs +45 -9
  19. package/fesm2022/caribeng-ui-chat.mjs +2 -2
  20. package/fesm2022/{caribeng-ui-caribe-coachmark.mjs → caribeng-ui-coachmark.mjs} +85 -85
  21. package/fesm2022/caribeng-ui-command-palette.mjs +2 -2
  22. package/fesm2022/caribeng-ui-comments.mjs +864 -0
  23. package/fesm2022/caribeng-ui-datagrid.mjs +1635 -0
  24. package/fesm2022/caribeng-ui-dataview.mjs +2 -2
  25. package/fesm2022/caribeng-ui-datepicker.mjs +2 -2
  26. package/fesm2022/caribeng-ui-document.mjs +495 -0
  27. package/fesm2022/{caribeng-ui-caribe-dots-indicator.mjs → caribeng-ui-dots-indicator.mjs} +41 -41
  28. package/fesm2022/caribeng-ui-dynamic-form.mjs +1581 -0
  29. package/fesm2022/caribeng-ui-editor.mjs +2303 -1311
  30. package/fesm2022/{caribeng-ui-caribe-expansion-panel.mjs → caribeng-ui-expansion-panel.mjs} +39 -39
  31. package/fesm2022/{caribeng-ui-caribe-fab-menu.mjs → caribeng-ui-fab-menu.mjs} +75 -75
  32. package/fesm2022/caribeng-ui-file-card.mjs +2 -2
  33. package/fesm2022/{caribeng-ui-caribe-file-download.mjs → caribeng-ui-file-download.mjs} +63 -63
  34. package/fesm2022/caribeng-ui-financial-report.mjs +380 -0
  35. package/fesm2022/{caribeng-ui-caribe-float-menu.mjs → caribeng-ui-float-menu.mjs} +31 -31
  36. package/fesm2022/caribeng-ui-floatlabel.mjs +2 -2
  37. package/fesm2022/caribeng-ui-flow.mjs +1508 -0
  38. package/fesm2022/{caribeng-ui-caribe-footer.mjs → caribeng-ui-footer.mjs} +19 -19
  39. package/fesm2022/caribeng-ui-gantt.mjs +1094 -0
  40. package/fesm2022/{caribeng-ui-caribe-header.mjs → caribeng-ui-header.mjs} +49 -49
  41. package/fesm2022/{caribeng-ui-caribe-icon-button.mjs → caribeng-ui-icon-button.mjs} +23 -23
  42. package/fesm2022/caribeng-ui-image-cropper.mjs +671 -0
  43. package/fesm2022/caribeng-ui-import-wizard.mjs +746 -0
  44. package/fesm2022/caribeng-ui-infinite-scroll.mjs +226 -0
  45. package/fesm2022/caribeng-ui-input-number.mjs +22 -6
  46. package/fesm2022/{caribeng-ui-caribe-input-search.mjs → caribeng-ui-input-search.mjs} +65 -65
  47. package/fesm2022/{caribeng-ui-caribe-input-token.mjs → caribeng-ui-input-token.mjs} +59 -59
  48. package/fesm2022/caribeng-ui-inputotp.mjs +2 -2
  49. package/fesm2022/caribeng-ui-journal-entry.mjs +340 -0
  50. package/fesm2022/caribeng-ui-kanban.mjs +6 -6
  51. package/fesm2022/caribeng-ui-kpi.mjs +189 -0
  52. package/fesm2022/{caribeng-ui-caribe-link.mjs → caribeng-ui-link.mjs} +37 -37
  53. package/fesm2022/caribeng-ui-masonry.mjs +93 -0
  54. package/fesm2022/caribeng-ui-media-player.mjs +821 -0
  55. package/fesm2022/caribeng-ui-mentions.mjs +2 -2
  56. package/fesm2022/caribeng-ui-notification-center.mjs +636 -0
  57. package/fesm2022/{caribeng-ui-caribe-off-canvas.mjs → caribeng-ui-off-canvas.mjs} +55 -55
  58. package/fesm2022/{caribeng-ui-caribe-onboarding.mjs → caribeng-ui-onboarding.mjs} +105 -105
  59. package/fesm2022/caribeng-ui-orderlist.mjs +2 -2
  60. package/fesm2022/caribeng-ui-pdfviewer.mjs +1755 -25
  61. package/fesm2022/caribeng-ui-period-picker.mjs +637 -0
  62. package/fesm2022/caribeng-ui-phone-input.mjs +2 -2
  63. package/fesm2022/caribeng-ui-picklist.mjs +2 -2
  64. package/fesm2022/caribeng-ui-pivot.mjs +419 -0
  65. package/fesm2022/caribeng-ui-pricing.mjs +2 -2
  66. package/fesm2022/caribeng-ui-rating.mjs +14 -17
  67. package/fesm2022/caribeng-ui-reconciliation.mjs +386 -0
  68. package/fesm2022/caribeng-ui-scheduler.mjs +1027 -0
  69. package/fesm2022/caribeng-ui-select.mjs +2 -2
  70. package/fesm2022/{caribeng-ui-caribe-shortcut.mjs → caribeng-ui-shortcut.mjs} +29 -29
  71. package/fesm2022/caribeng-ui-sidebar.mjs +2 -2
  72. package/fesm2022/caribeng-ui-signature-pad.mjs +654 -0
  73. package/fesm2022/{caribeng-ui-caribe-status.mjs → caribeng-ui-status.mjs} +11 -11
  74. package/fesm2022/{caribeng-ui-caribe-step-indicator.mjs → caribeng-ui-step-indicator.mjs} +53 -53
  75. package/fesm2022/caribeng-ui-stepper.mjs +2 -2
  76. package/fesm2022/caribeng-ui-steps.mjs +2 -2
  77. package/fesm2022/caribeng-ui-table.mjs +323 -15
  78. package/fesm2022/caribeng-ui-tabmenu.mjs +2 -2
  79. package/fesm2022/caribeng-ui-tabs.mjs +2 -2
  80. package/fesm2022/{caribeng-ui-caribe-tag-button.mjs → caribeng-ui-tag-button.mjs} +25 -25
  81. package/fesm2022/caribeng-ui-tag.mjs +2 -2
  82. package/fesm2022/caribeng-ui-tieredmenu.mjs +6 -6
  83. package/fesm2022/{caribeng-ui-caribe-tracker.mjs → caribeng-ui-tracker.mjs} +29 -29
  84. package/fesm2022/{caribeng-ui-caribe-transaction-status.mjs → caribeng-ui-transaction-status.mjs} +11 -11
  85. package/fesm2022/caribeng-ui-treeselect.mjs +44 -9
  86. package/fesm2022/caribeng-ui-treetable.mjs +236 -17
  87. package/fesm2022/{caribeng-ui-caribe-voucher.mjs → caribeng-ui-voucher.mjs} +45 -45
  88. package/fesm2022/caribeng-ui-xlsx.mjs +486 -0
  89. package/fesm2022/caribeng-ui.mjs +1181 -1
  90. package/package.json +217 -177
  91. package/types/caribeng-ui-account-picker.d.ts +142 -0
  92. package/types/caribeng-ui-aging.d.ts +123 -0
  93. package/types/{caribeng-ui-caribe-alert.d.ts → caribeng-ui-alert.d.ts} +2 -2
  94. package/types/caribeng-ui-amount.d.ts +82 -0
  95. package/types/{caribeng-ui-caribe-animation-lottie.d.ts → caribeng-ui-animation-lottie.d.ts} +2 -2
  96. package/types/caribeng-ui-audit-log.d.ts +86 -0
  97. package/types/caribeng-ui-breadcrumb.d.ts +2 -2
  98. package/types/{caribeng-ui-caribe-button-group.d.ts → caribeng-ui-button-group.d.ts} +2 -2
  99. package/types/{caribeng-ui-caribe-card-content.d.ts → caribeng-ui-card-content.d.ts} +2 -2
  100. package/types/{caribeng-ui-caribe-card-move.d.ts → caribeng-ui-card-move.d.ts} +1 -1
  101. package/types/{caribeng-ui-caribe-card-primary.d.ts → caribeng-ui-card-primary.d.ts} +2 -2
  102. package/types/{caribeng-ui-caribe-card-select.d.ts → caribeng-ui-card-select.d.ts} +2 -2
  103. package/types/caribeng-ui-cascadeselect.d.ts +6 -0
  104. package/types/{caribeng-ui-caribe-coachmark.d.ts → caribeng-ui-coachmark.d.ts} +2 -2
  105. package/types/caribeng-ui-comments.d.ts +214 -0
  106. package/types/caribeng-ui-datagrid.d.ts +269 -0
  107. package/types/caribeng-ui-document.d.ts +138 -0
  108. package/types/{caribeng-ui-caribe-dots-indicator.d.ts → caribeng-ui-dots-indicator.d.ts} +2 -2
  109. package/types/caribeng-ui-dynamic-form.d.ts +409 -0
  110. package/types/caribeng-ui-editor.d.ts +110 -12
  111. package/types/{caribeng-ui-caribe-expansion-panel.d.ts → caribeng-ui-expansion-panel.d.ts} +1 -1
  112. package/types/{caribeng-ui-caribe-fab-menu.d.ts → caribeng-ui-fab-menu.d.ts} +2 -2
  113. package/types/{caribeng-ui-caribe-file-download.d.ts → caribeng-ui-file-download.d.ts} +1 -1
  114. package/types/caribeng-ui-financial-report.d.ts +102 -0
  115. package/types/{caribeng-ui-caribe-float-menu.d.ts → caribeng-ui-float-menu.d.ts} +2 -2
  116. package/types/caribeng-ui-flow.d.ts +228 -0
  117. package/types/{caribeng-ui-caribe-footer.d.ts → caribeng-ui-footer.d.ts} +2 -2
  118. package/types/caribeng-ui-gantt.d.ts +244 -0
  119. package/types/{caribeng-ui-caribe-header.d.ts → caribeng-ui-header.d.ts} +2 -2
  120. package/types/{caribeng-ui-caribe-icon-button.d.ts → caribeng-ui-icon-button.d.ts} +2 -2
  121. package/types/{caribeng-ui-caribe-illustration.d.ts → caribeng-ui-illustration.d.ts} +1 -1
  122. package/types/caribeng-ui-image-cropper.d.ts +169 -0
  123. package/types/caribeng-ui-import-wizard.d.ts +149 -0
  124. package/types/caribeng-ui-infinite-scroll.d.ts +75 -0
  125. package/types/caribeng-ui-input-number.d.ts +1 -1
  126. package/types/{caribeng-ui-caribe-input-search.d.ts → caribeng-ui-input-search.d.ts} +2 -2
  127. package/types/{caribeng-ui-caribe-input-token.d.ts → caribeng-ui-input-token.d.ts} +2 -2
  128. package/types/caribeng-ui-journal-entry.d.ts +110 -0
  129. package/types/caribeng-ui-kpi.d.ts +64 -0
  130. package/types/{caribeng-ui-caribe-link.d.ts → caribeng-ui-link.d.ts} +2 -2
  131. package/types/{caribeng-ui-caribe-logo.d.ts → caribeng-ui-logo.d.ts} +1 -1
  132. package/types/caribeng-ui-masonry.d.ts +32 -0
  133. package/types/caribeng-ui-media-player.d.ts +140 -0
  134. package/types/caribeng-ui-notification-center.d.ts +153 -0
  135. package/types/{caribeng-ui-caribe-off-canvas.d.ts → caribeng-ui-off-canvas.d.ts} +2 -2
  136. package/types/{caribeng-ui-caribe-onboarding.d.ts → caribeng-ui-onboarding.d.ts} +2 -2
  137. package/types/caribeng-ui-pdfviewer.d.ts +245 -5
  138. package/types/caribeng-ui-period-picker.d.ts +148 -0
  139. package/types/caribeng-ui-pivot.d.ts +116 -0
  140. package/types/caribeng-ui-reconciliation.d.ts +109 -0
  141. package/types/caribeng-ui-scheduler.d.ts +188 -0
  142. package/types/{caribeng-ui-caribe-shortcut.d.ts → caribeng-ui-shortcut.d.ts} +2 -2
  143. package/types/caribeng-ui-signature-pad.d.ts +151 -0
  144. package/types/{caribeng-ui-caribe-status.d.ts → caribeng-ui-status.d.ts} +2 -2
  145. package/types/{caribeng-ui-caribe-step-indicator.d.ts → caribeng-ui-step-indicator.d.ts} +2 -2
  146. package/types/caribeng-ui-table.d.ts +66 -2
  147. package/types/{caribeng-ui-caribe-tag-button.d.ts → caribeng-ui-tag-button.d.ts} +2 -2
  148. package/types/{caribeng-ui-caribe-tracker.d.ts → caribeng-ui-tracker.d.ts} +2 -2
  149. package/types/{caribeng-ui-caribe-transaction-status.d.ts → caribeng-ui-transaction-status.d.ts} +2 -2
  150. package/types/caribeng-ui-treeselect.d.ts +6 -0
  151. package/types/caribeng-ui-treetable.d.ts +67 -2
  152. package/types/{caribeng-ui-caribe-voucher.d.ts → caribeng-ui-voucher.d.ts} +2 -2
  153. package/types/caribeng-ui-xlsx.d.ts +79 -0
  154. package/types/caribeng-ui.d.ts +711 -2
  155. package/fesm2022/caribeng-ui-caribe-calendar.mjs +0 -92
  156. package/fesm2022/caribeng-ui-caribe-circle-loading.mjs +0 -130
  157. package/fesm2022/caribeng-ui-caribe-data-table.mjs +0 -94
  158. package/fesm2022/caribeng-ui-caribe-file-select.mjs +0 -88
  159. package/fesm2022/caribeng-ui-caribe-inline-alert.mjs +0 -100
  160. package/fesm2022/caribeng-ui-caribe-input-date.mjs +0 -138
  161. package/fesm2022/caribeng-ui-caribe-input-select.mjs +0 -154
  162. package/fesm2022/caribeng-ui-caribe-loader.mjs +0 -48
  163. package/fesm2022/caribeng-ui-caribe-modal.mjs +0 -119
  164. package/fesm2022/caribeng-ui-caribe-pictogram.mjs +0 -52
  165. package/fesm2022/caribeng-ui-caribe-pop-over.mjs +0 -104
  166. package/fesm2022/caribeng-ui-caribe-preloader.mjs +0 -57
  167. package/fesm2022/caribeng-ui-caribe-progress-bar.mjs +0 -84
  168. package/fesm2022/caribeng-ui-caribe-stroke.mjs +0 -69
  169. package/fesm2022/caribeng-ui-caribe-switch.mjs +0 -182
  170. package/types/caribeng-ui-caribe-calendar.d.ts +0 -27
  171. package/types/caribeng-ui-caribe-circle-loading.d.ts +0 -28
  172. package/types/caribeng-ui-caribe-data-table.d.ts +0 -26
  173. package/types/caribeng-ui-caribe-file-select.d.ts +0 -31
  174. package/types/caribeng-ui-caribe-inline-alert.d.ts +0 -28
  175. package/types/caribeng-ui-caribe-input-date.d.ts +0 -40
  176. package/types/caribeng-ui-caribe-input-select.d.ts +0 -47
  177. package/types/caribeng-ui-caribe-loader.d.ts +0 -22
  178. package/types/caribeng-ui-caribe-modal.d.ts +0 -21
  179. package/types/caribeng-ui-caribe-pictogram.d.ts +0 -15
  180. package/types/caribeng-ui-caribe-pop-over.d.ts +0 -17
  181. package/types/caribeng-ui-caribe-preloader.d.ts +0 -24
  182. package/types/caribeng-ui-caribe-progress-bar.d.ts +0 -28
  183. package/types/caribeng-ui-caribe-stroke.d.ts +0 -17
  184. package/types/caribeng-ui-caribe-switch.d.ts +0 -55
  185. /package/fesm2022/{caribeng-ui-caribe-illustration.mjs → caribeng-ui-illustration.mjs} +0 -0
  186. /package/fesm2022/{caribeng-ui-caribe-logo.mjs → caribeng-ui-logo.mjs} +0 -0
@@ -0,0 +1,1508 @@
1
+ import * as i0 from '@angular/core';
2
+ import { model, input, booleanAttribute, numberAttribute, output, inject, ElementRef, Injector, viewChild, signal, computed, effect, untracked, afterNextRender, ViewEncapsulation, ChangeDetectionStrategy, Component } from '@angular/core';
3
+ import { injectCbTranslation, cbFormat } from '@caribeng/ui';
4
+ import { CbIconRegistry, CbIcon } from '@caribeng/ui/icon';
5
+ import { spline, workflow, scan, zoomOut, zoomIn } from '@caribeng/ui/icons';
6
+
7
+ /** Tamaño de cada tipo de nodo (px a zoom 100 %). */
8
+ const CB_FLOW_NODE_SIZE = {
9
+ start: { w: 136, h: 44 },
10
+ end: { w: 136, h: 44 },
11
+ task: { w: 176, h: 60 },
12
+ decision: { w: 160, h: 92 },
13
+ };
14
+ const EMPTY_FLOW = { nodes: [], edges: [] };
15
+ /**
16
+ * Problemas del flujo: falta un inicio o un fin, nodos sin salida o inalcanzables desde el inicio,
17
+ * decisiones con menos de dos salidas (o salidas sin etiqueta), y conexiones que entran en un inicio
18
+ * o salen de un fin. Un flujo vacío no tiene problemas.
19
+ */
20
+ function cbValidateFlow(flow) {
21
+ const issues = [];
22
+ if (!flow.nodes.length)
23
+ return issues;
24
+ const out = adjacency(flow);
25
+ const incoming = new Map();
26
+ for (const e of flow.edges)
27
+ incoming.set(e.to, (incoming.get(e.to) ?? 0) + 1);
28
+ const starts = flow.nodes.filter((n) => n.type === 'start');
29
+ if (!starts.length)
30
+ issues.push({ code: 'no-start', severity: 'error' });
31
+ for (const extra of starts.slice(1))
32
+ issues.push({ code: 'many-starts', nodeId: extra.id, severity: 'error' });
33
+ if (!flow.nodes.some((n) => n.type === 'end'))
34
+ issues.push({ code: 'no-end', severity: 'error' });
35
+ const reachable = new Set();
36
+ const queue = starts.map((s) => s.id);
37
+ while (queue.length) {
38
+ const id = queue.shift();
39
+ if (reachable.has(id))
40
+ continue;
41
+ reachable.add(id);
42
+ for (const e of out.get(id) ?? [])
43
+ queue.push(e.to);
44
+ }
45
+ for (const n of flow.nodes) {
46
+ const exits = out.get(n.id) ?? [];
47
+ if (n.type === 'start' && incoming.get(n.id))
48
+ issues.push({ code: 'start-incoming', nodeId: n.id, severity: 'error' });
49
+ if (n.type === 'end' && exits.length)
50
+ issues.push({ code: 'end-outgoing', nodeId: n.id, severity: 'error' });
51
+ if (n.type !== 'end' && !exits.length)
52
+ issues.push({ code: 'dead-end', nodeId: n.id, severity: 'error' });
53
+ if (n.type === 'decision' && exits.length === 1)
54
+ issues.push({ code: 'decision-branches', nodeId: n.id, severity: 'error' });
55
+ if (n.type === 'decision' && exits.length > 1 && exits.some((e) => !e.label?.trim())) {
56
+ issues.push({ code: 'branch-label', nodeId: n.id, severity: 'warning' });
57
+ }
58
+ if (starts.length && !reachable.has(n.id))
59
+ issues.push({ code: 'unreachable', nodeId: n.id, severity: 'error' });
60
+ }
61
+ return issues;
62
+ }
63
+ /** Si se puede conectar `from` con `to`: no consigo mismo, no repetida, no hacia un inicio ni desde un fin. */
64
+ function canConnect(flow, from, to) {
65
+ if (from === to)
66
+ return false;
67
+ const a = flow.nodes.find((n) => n.id === from);
68
+ const b = flow.nodes.find((n) => n.id === to);
69
+ if (!a || !b || a.type === 'end' || b.type === 'start')
70
+ return false;
71
+ return !flow.edges.some((e) => e.from === from && e.to === to);
72
+ }
73
+ /** Orden de lectura: recorrido en anchura desde los inicios y, después, lo que quede. */
74
+ function readingOrder(flow) {
75
+ const out = adjacency(flow);
76
+ const byId = new Map(flow.nodes.map((n) => [n.id, n]));
77
+ const seen = new Set();
78
+ const order = [];
79
+ const visit = (roots) => {
80
+ const queue = [...roots];
81
+ while (queue.length) {
82
+ const id = queue.shift();
83
+ if (seen.has(id) || !byId.has(id))
84
+ continue;
85
+ seen.add(id);
86
+ order.push(byId.get(id));
87
+ for (const e of out.get(id) ?? [])
88
+ queue.push(e.to);
89
+ }
90
+ };
91
+ visit(flow.nodes.filter((n) => n.type === 'start').map((n) => n.id));
92
+ for (const n of flow.nodes)
93
+ visit([n.id]);
94
+ return order;
95
+ }
96
+ /**
97
+ * Coloca los nodos por niveles de arriba abajo (el inicio arriba y cada paso debajo del anterior),
98
+ * centrados y sin cruzar más de lo necesario. Las conexiones que vuelven atrás no cuentan para
99
+ * los niveles. Las posiciones se ajustan a la rejilla `grid`.
100
+ */
101
+ function cbLayoutFlow(flow, grid = 16) {
102
+ if (!flow.nodes.length)
103
+ return flow;
104
+ const order = readingOrder(flow);
105
+ const index = new Map(order.map((n, i) => [n.id, i]));
106
+ // Conexiones hacia delante: las que no vuelven a un nodo anterior en el orden de lectura.
107
+ const forward = flow.edges.filter((e) => (index.get(e.from) ?? 0) < (index.get(e.to) ?? 0));
108
+ const level = new Map(order.map((n) => [n.id, 0]));
109
+ for (const n of order) {
110
+ for (const e of forward)
111
+ if (e.from === n.id)
112
+ level.set(e.to, Math.max(level.get(e.to), level.get(n.id) + 1));
113
+ }
114
+ const rows = [];
115
+ for (const n of order)
116
+ (rows[level.get(n.id)] ??= []).push(n);
117
+ // Orden dentro de cada nivel: el medio de sus padres (baricentro), en dos pasadas.
118
+ const slot = new Map();
119
+ rows.forEach((row) => row.forEach((n, i) => slot.set(n.id, i)));
120
+ for (let pass = 0; pass < 2; pass++) {
121
+ for (const row of rows.slice(1)) {
122
+ const bary = (n) => {
123
+ const parents = forward.filter((e) => e.to === n.id).map((e) => slot.get(e.from) ?? 0);
124
+ return parents.length ? parents.reduce((a, b) => a + b, 0) / parents.length : slot.get(n.id);
125
+ };
126
+ row.sort((a, b) => bary(a) - bary(b));
127
+ row.forEach((n, i) => slot.set(n.id, i));
128
+ }
129
+ }
130
+ const gapX = 48;
131
+ const gapY = 64;
132
+ const pos = new Map();
133
+ let y = 0;
134
+ for (const row of rows) {
135
+ const height = Math.max(...row.map((n) => CB_FLOW_NODE_SIZE[n.type].h));
136
+ const width = row.reduce((s, n) => s + CB_FLOW_NODE_SIZE[n.type].w, 0) + gapX * (row.length - 1);
137
+ let x = -width / 2;
138
+ for (const n of row) {
139
+ const size = CB_FLOW_NODE_SIZE[n.type];
140
+ pos.set(n.id, { x: snap(x, grid), y: snap(y + (height - size.h) / 2, grid) });
141
+ x += size.w + gapX;
142
+ }
143
+ y += height + gapY;
144
+ }
145
+ return { ...flow, nodes: flow.nodes.map((n) => ({ ...n, ...pos.get(n.id) })) };
146
+ }
147
+ /** Ajusta a la rejilla. */
148
+ function snap(value, grid) {
149
+ return grid > 1 ? Math.round(value / grid) * grid : Math.round(value);
150
+ }
151
+ /**
152
+ * Curva de una conexión entre los lados enfrentados de los nodos. Hacia abajo sale por abajo y
153
+ * entra por arriba; en horizontal, por los lados; si vuelve hacia arriba (un bucle), rodea por la
154
+ * derecha para no cruzar los nodos de en medio.
155
+ */
156
+ function edgeGeometry(from, to) {
157
+ const a = centerOf(from);
158
+ const b = centerOf(to);
159
+ const [dx, dy] = [b[0] - a[0], b[1] - a[1]];
160
+ let s;
161
+ let t;
162
+ let c1;
163
+ let c2;
164
+ if (Math.abs(dy) >= Math.abs(dx) * 0.6 && dy > 0) {
165
+ s = sidePoint(from, 'bottom');
166
+ t = sidePoint(to, 'top');
167
+ const k = Math.max(24, (t[1] - s[1]) / 2);
168
+ [c1, c2] = [[s[0], s[1] + k], [t[0], t[1] - k]];
169
+ }
170
+ else if (Math.abs(dy) >= Math.abs(dx) * 0.6) {
171
+ // Hacia arriba: rodea por la derecha.
172
+ s = sidePoint(from, 'right');
173
+ t = sidePoint(to, 'right');
174
+ const bulge = Math.max(s[0], t[0]) + 56;
175
+ [c1, c2] = [[bulge, s[1]], [bulge, t[1]]];
176
+ }
177
+ else {
178
+ const right = dx > 0;
179
+ s = sidePoint(from, right ? 'right' : 'left');
180
+ t = sidePoint(to, right ? 'left' : 'right');
181
+ const k = Math.max(24, Math.abs(t[0] - s[0]) / 2) * (right ? 1 : -1);
182
+ [c1, c2] = [[s[0] + k, s[1]], [t[0] - k, t[1]]];
183
+ }
184
+ const mid = [(s[0] + 3 * c1[0] + 3 * c2[0] + t[0]) / 8, (s[1] + 3 * c1[1] + 3 * c2[1] + t[1]) / 8];
185
+ // La flecha sigue la tangente final de la curva (de c2 a t).
186
+ const len = Math.hypot(t[0] - c2[0], t[1] - c2[1]) || 1;
187
+ const [ux, uy] = [(t[0] - c2[0]) / len, (t[1] - c2[1]) / len];
188
+ const [bx, by] = [t[0] - ux * 10, t[1] - uy * 10];
189
+ const arrow = [[t[0], t[1]], [bx - uy * 5, by + ux * 5], [bx + uy * 5, by - ux * 5]].map(([x, y]) => `${r(x)},${r(y)}`).join(' ');
190
+ return { d: `M${r(s[0])} ${r(s[1])} C${r(c1[0])} ${r(c1[1])} ${r(c2[0])} ${r(c2[1])} ${r(t[0])} ${r(t[1])}`, mid, arrow };
191
+ }
192
+ /** Caja que contiene a todos los nodos (o `null` si no hay). */
193
+ function flowBounds(nodes) {
194
+ if (!nodes.length)
195
+ return null;
196
+ const x = Math.min(...nodes.map((n) => n.x));
197
+ const y = Math.min(...nodes.map((n) => n.y));
198
+ const right = Math.max(...nodes.map((n) => n.x + CB_FLOW_NODE_SIZE[n.type].w));
199
+ const bottom = Math.max(...nodes.map((n) => n.y + CB_FLOW_NODE_SIZE[n.type].h));
200
+ return { x, y, w: right - x, h: bottom - y };
201
+ }
202
+ function centerOf(n) {
203
+ const size = CB_FLOW_NODE_SIZE[n.type];
204
+ return [n.x + size.w / 2, n.y + size.h / 2];
205
+ }
206
+ function sidePoint(n, side) {
207
+ const { w, h } = CB_FLOW_NODE_SIZE[n.type];
208
+ switch (side) {
209
+ case 'top':
210
+ return [n.x + w / 2, n.y];
211
+ case 'bottom':
212
+ return [n.x + w / 2, n.y + h];
213
+ case 'left':
214
+ return [n.x, n.y + h / 2];
215
+ case 'right':
216
+ return [n.x + w, n.y + h / 2];
217
+ }
218
+ }
219
+ function adjacency(flow) {
220
+ const out = new Map();
221
+ for (const e of flow.edges)
222
+ (out.get(e.from) ?? out.set(e.from, []).get(e.from)).push(e);
223
+ return out;
224
+ }
225
+ function r(v) {
226
+ return Math.round(v * 10) / 10;
227
+ }
228
+
229
+ let nextUid = 0;
230
+ let nextId = 0;
231
+ const MIN_ZOOM = 0.25;
232
+ const MAX_ZOOM = 2;
233
+ const ZOOM_STEP = 1.2;
234
+ const FLOW_ICONS = { 'zoom-in': zoomIn, 'zoom-out': zoomOut, scan, workflow, spline };
235
+ /**
236
+ * Editor de flujos (aprobaciones, procesos): nodos de inicio, tarea, decisión y fin unidos por
237
+ * conexiones con etiqueta. Se arrastran los nodos, se conectan desde su tirador y el lienzo se
238
+ * desplaza y amplía. Todo se puede hacer con el teclado, y la vista Lista es una alternativa
239
+ * sin lienzo para editar el flujo con lector de pantalla. Valida el flujo (`issues`) y lo ordena
240
+ * por niveles (`layout()`).
241
+ *
242
+ * @example
243
+ * ```html
244
+ * <cb-flow-editor [(flow)]="aprobacion" />
245
+ * ```
246
+ */
247
+ class CbFlowEditor {
248
+ constructor() {
249
+ /** Textos de interfaz en el idioma activo (`provideCaribeNG({ locale })`). */
250
+ this.t = injectCbTranslation();
251
+ this.format = cbFormat;
252
+ /** El flujo: nodos y conexiones. */
253
+ this.flow = model(EMPTY_FLOW, /* @ts-ignore */
254
+ ...(ngDevMode ? [{ debugName: "flow" }] : /* istanbul ignore next */ []));
255
+ /** Solo lectura: se puede recorrer, desplazar y ampliar, pero no editar. Intro abre el nodo (`nodeOpen`). */
256
+ this.readonly = input(false, { ...(ngDevMode ? { debugName: "readonly" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
257
+ /** Rejilla en px: los nodos se ajustan a ella al moverlos (1 la desactiva). */
258
+ this.gridSize = input(16, { ...(ngDevMode ? { debugName: "gridSize" } : /* istanbul ignore next */ {}), transform: numberAttribute });
259
+ /** Zoom (1 = 100 %). */
260
+ this.zoom = model(1, /* @ts-ignore */
261
+ ...(ngDevMode ? [{ debugName: "zoom" }] : /* istanbul ignore next */ []));
262
+ /** Diagrama o lista (alternativa sin lienzo, para teclado y lector de pantalla). */
263
+ this.view = model('diagram', /* @ts-ignore */
264
+ ...(ngDevMode ? [{ debugName: "view" }] : /* istanbul ignore next */ []));
265
+ /** Nodo o conexión seleccionada (su `id`). */
266
+ this.selected = model(null, /* @ts-ignore */
267
+ ...(ngDevMode ? [{ debugName: "selected" }] : /* istanbul ignore next */ []));
268
+ /** Muestra los problemas del flujo bajo el editor. */
269
+ this.showValidation = input(true, { ...(ngDevMode ? { debugName: "showValidation" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
270
+ /** Ajusta el zoom para ver todo el flujo al cargarlo. */
271
+ this.fitOnLoad = input(true, { ...(ngDevMode ? { debugName: "fitOnLoad" } : /* istanbul ignore next */ {}), transform: booleanAttribute });
272
+ /** Nombre accesible (por defecto, "Editor de flujo"). */
273
+ this.ariaLabel = input('', /* @ts-ignore */
274
+ ...(ngDevMode ? [{ debugName: "ariaLabel" }] : /* istanbul ignore next */ []));
275
+ /** Se abrió un nodo: doble clic, o Intro en solo lectura. */
276
+ this.nodeOpen = output();
277
+ this.uid = `cb-flow-${++nextUid}`;
278
+ this.types = ['start', 'task', 'decision', 'end'];
279
+ this.sizes = CB_FLOW_NODE_SIZE;
280
+ this.minZoom = MIN_ZOOM;
281
+ this.maxZoom = MAX_ZOOM;
282
+ this.step = ZOOM_STEP;
283
+ this.host = inject(ElementRef);
284
+ this.injector = inject(Injector);
285
+ this.viewport = viewChild('viewport', /* @ts-ignore */
286
+ ...(ngDevMode ? [{ debugName: "viewport" }] : /* istanbul ignore next */ []));
287
+ this.editor = viewChild('editor', /* @ts-ignore */
288
+ ...(ngDevMode ? [{ debugName: "editor" }] : /* istanbul ignore next */ []));
289
+ /** Desplazamiento del lienzo (px de pantalla). */
290
+ this.pan = signal({ x: 0, y: 0 }, /* @ts-ignore */
291
+ ...(ngDevMode ? [{ debugName: "pan" }] : /* istanbul ignore next */ []));
292
+ this.panning = signal(false, /* @ts-ignore */
293
+ ...(ngDevMode ? [{ debugName: "panning" }] : /* istanbul ignore next */ []));
294
+ /** Posición provisional de un nodo mientras se arrastra. */
295
+ this.dragPos = signal(null, /* @ts-ignore */
296
+ ...(ngDevMode ? [{ debugName: "dragPos" }] : /* istanbul ignore next */ []));
297
+ /** Nodo desde el que se conecta (con el teclado o con el botón Conectar). */
298
+ this.connectFrom = signal(null, /* @ts-ignore */
299
+ ...(ngDevMode ? [{ debugName: "connectFrom" }] : /* istanbul ignore next */ []));
300
+ /** Conexión que se arrastra desde un tirador, hasta el puntero (coordenadas del lienzo). */
301
+ this.linkDrag = signal(null, /* @ts-ignore */
302
+ ...(ngDevMode ? [{ debugName: "linkDrag" }] : /* istanbul ignore next */ []));
303
+ this.editing = signal(null, /* @ts-ignore */
304
+ ...(ngDevMode ? [{ debugName: "editing" }] : /* istanbul ignore next */ []));
305
+ this.announcement = signal('', /* @ts-ignore */
306
+ ...(ngDevMode ? [{ debugName: "announcement" }] : /* istanbul ignore next */ []));
307
+ this.announceFlip = false;
308
+ this.fitted = false;
309
+ /** Problemas del flujo (los mismos que muestra el editor). */
310
+ this.issues = computed(() => cbValidateFlow(this.flow()), /* @ts-ignore */
311
+ ...(ngDevMode ? [{ debugName: "issues" }] : /* istanbul ignore next */ []));
312
+ this.nodes = computed(() => {
313
+ const drag = this.dragPos();
314
+ const nodes = this.flow().nodes;
315
+ return drag ? nodes.map((n) => (n.id === drag.id ? { ...n, x: drag.x, y: drag.y } : n)) : nodes;
316
+ }, /* @ts-ignore */
317
+ ...(ngDevMode ? [{ debugName: "nodes" }] : /* istanbul ignore next */ []));
318
+ this.nodeMap = computed(() => new Map(this.nodes().map((n) => [n.id, n])), /* @ts-ignore */
319
+ ...(ngDevMode ? [{ debugName: "nodeMap" }] : /* istanbul ignore next */ []));
320
+ this.outgoing = computed(() => {
321
+ const map = new Map();
322
+ for (const e of this.flow().edges)
323
+ (map.get(e.from) ?? map.set(e.from, []).get(e.from)).push(e);
324
+ return map;
325
+ }, /* @ts-ignore */
326
+ ...(ngDevMode ? [{ debugName: "outgoing" }] : /* istanbul ignore next */ []));
327
+ this.issuesByNode = computed(() => {
328
+ const map = new Map();
329
+ for (const issue of this.issues())
330
+ if (issue.nodeId)
331
+ (map.get(issue.nodeId) ?? map.set(issue.nodeId, []).get(issue.nodeId)).push(issue);
332
+ return map;
333
+ }, /* @ts-ignore */
334
+ ...(ngDevMode ? [{ debugName: "issuesByNode" }] : /* istanbul ignore next */ []));
335
+ this.edgeViews = computed(() => {
336
+ const nodes = this.nodeMap();
337
+ const views = [];
338
+ for (const edge of this.flow().edges) {
339
+ const from = nodes.get(edge.from);
340
+ const to = nodes.get(edge.to);
341
+ if (!from || !to)
342
+ continue;
343
+ const g = edgeGeometry(from, to);
344
+ const name = cbFormat(this.t().flowEdgeOf, from.label, to.label);
345
+ views.push({ edge, d: g.d, arrow: g.arrow, mid: g.mid, label: edge.label ? `${name}: ${edge.label}` : name });
346
+ }
347
+ return views;
348
+ }, /* @ts-ignore */
349
+ ...(ngDevMode ? [{ debugName: "edgeViews" }] : /* istanbul ignore next */ []));
350
+ /** Caja del SVG de conexiones: la de los nodos con margen para las curvas que rodean. */
351
+ this.svgBox = computed(() => {
352
+ const b = flowBounds(this.nodes()) ?? { x: 0, y: 0, w: 0, h: 0 };
353
+ const drag = this.linkDrag();
354
+ const [x1, y1] = drag ? [Math.min(b.x, drag.x), Math.min(b.y, drag.y)] : [b.x, b.y];
355
+ const [x2, y2] = drag ? [Math.max(b.x + b.w, drag.x), Math.max(b.y + b.h, drag.y)] : [b.x + b.w, b.y + b.h];
356
+ return { x: x1 - 160, y: y1 - 160, w: x2 - x1 + 320, h: y2 - y1 + 320 };
357
+ }, /* @ts-ignore */
358
+ ...(ngDevMode ? [{ debugName: "svgBox" }] : /* istanbul ignore next */ []));
359
+ this.tempLine = computed(() => {
360
+ const drag = this.linkDrag();
361
+ const from = drag && this.nodeMap().get(drag.from);
362
+ if (!drag || !from)
363
+ return '';
364
+ const { w, h } = CB_FLOW_NODE_SIZE[from.type];
365
+ return `M${from.x + w / 2} ${from.y + h} L${drag.x} ${drag.y}`;
366
+ }, /* @ts-ignore */
367
+ ...(ngDevMode ? [{ debugName: "tempLine" }] : /* istanbul ignore next */ []));
368
+ this.worldTransform = computed(() => {
369
+ const { x, y } = this.pan();
370
+ return `translate(${x}px, ${y}px) scale(${this.zoom()})`;
371
+ }, /* @ts-ignore */
372
+ ...(ngDevMode ? [{ debugName: "worldTransform" }] : /* istanbul ignore next */ []));
373
+ this.gridBackground = computed(() => {
374
+ const size = Math.max(4, this.gridSize() * this.zoom());
375
+ const { x, y } = this.pan();
376
+ return { size: `${size}px ${size}px`, position: `${x}px ${y}px` };
377
+ }, /* @ts-ignore */
378
+ ...(ngDevMode ? [{ debugName: "gridBackground" }] : /* istanbul ignore next */ []));
379
+ this.percent = computed(() => `${Math.round(this.zoom() * 100)} %`, /* @ts-ignore */
380
+ ...(ngDevMode ? [{ debugName: "percent" }] : /* istanbul ignore next */ []));
381
+ this.ordered = computed(() => readingOrder(this.flow()), /* @ts-ignore */
382
+ ...(ngDevMode ? [{ debugName: "ordered" }] : /* istanbul ignore next */ []));
383
+ this.selectedNode = computed(() => {
384
+ const id = this.selected();
385
+ return id ? (this.flow().nodes.find((n) => n.id === id) ?? null) : null;
386
+ }, /* @ts-ignore */
387
+ ...(ngDevMode ? [{ debugName: "selectedNode" }] : /* istanbul ignore next */ []));
388
+ this.selectedExists = computed(() => {
389
+ const id = this.selected();
390
+ return !!id && (this.flow().nodes.some((n) => n.id === id) || this.flow().edges.some((e) => e.id === id));
391
+ }, /* @ts-ignore */
392
+ ...(ngDevMode ? [{ debugName: "selectedExists" }] : /* istanbul ignore next */ []));
393
+ /** Caja del campo de edición en línea (sobre el nodo o en el punto medio de la conexión). */
394
+ this.editorBox = computed(() => {
395
+ const editing = this.editing();
396
+ if (!editing)
397
+ return null;
398
+ if (editing.kind === 'node') {
399
+ const n = this.nodeMap().get(editing.id);
400
+ if (!n)
401
+ return null;
402
+ const { w, h } = CB_FLOW_NODE_SIZE[n.type];
403
+ const width = Math.min(w - 16, 160);
404
+ return { x: n.x + (w - width) / 2, y: n.y + h / 2 - 15, w: width, label: cbFormat(this.t().flowNameOf, this.typeLabel(n.type)) };
405
+ }
406
+ const e = this.edgeViews().find((v) => v.edge.id === editing.id);
407
+ if (!e)
408
+ return null;
409
+ return { x: e.mid[0] - 70, y: e.mid[1] - 15, w: 140, label: cbFormat(this.t().flowEdgeLabelOf, this.nameOf(e.edge.from), this.nameOf(e.edge.to)) };
410
+ }, /* @ts-ignore */
411
+ ...(ngDevMode ? [{ debugName: "editorBox" }] : /* istanbul ignore next */ []));
412
+ const registry = inject(CbIconRegistry);
413
+ for (const [name, body] of Object.entries(FLOW_ICONS)) {
414
+ if (!registry.has(name))
415
+ registry.register({ [name]: body });
416
+ }
417
+ // Ajusta el zoom la primera vez que hay nodos y lienzo.
418
+ effect(() => {
419
+ const hasNodes = this.flow().nodes.length > 0;
420
+ const viewport = this.viewport();
421
+ if (!hasNodes || !viewport || this.fitted || !untracked(this.fitOnLoad))
422
+ return;
423
+ this.fitted = true;
424
+ afterNextRender(() => this.fit(), { injector: this.injector });
425
+ });
426
+ // Un nodo o conexión que desaparece del flujo deja de estar seleccionado.
427
+ effect(() => {
428
+ const id = this.selected();
429
+ if (id && !this.selectedExists())
430
+ untracked(() => this.selected.set(null));
431
+ });
432
+ }
433
+ // --- API pública ------------------------------------------------------------
434
+ /** Añade un nodo. Con un nodo seleccionado, lo pone debajo y lo conecta desde él; después abre su nombre. */
435
+ addNode(type) {
436
+ const flow = this.flow();
437
+ const source = this.selectedNode();
438
+ const size = CB_FLOW_NODE_SIZE[type];
439
+ const grid = this.gridSize();
440
+ let x;
441
+ let y;
442
+ if (source) {
443
+ const s = CB_FLOW_NODE_SIZE[source.type];
444
+ // Debajo del nodo; si ya tiene salidas, a la derecha de la última.
445
+ const siblings = (this.outgoing().get(source.id) ?? []).map((e) => this.nodeMap().get(e.to)).filter((n) => !!n);
446
+ const below = source.y + s.h + 64;
447
+ const right = siblings.length ? Math.max(...siblings.map((n) => n.x + CB_FLOW_NODE_SIZE[n.type].w)) + 48 : null;
448
+ [x, y] = [right ?? source.x + (s.w - size.w) / 2, below];
449
+ }
450
+ else if (flow.nodes.length) {
451
+ const b = flowBounds(flow.nodes);
452
+ [x, y] = [b.x + (b.w - size.w) / 2, b.y + b.h + 64];
453
+ }
454
+ else {
455
+ [x, y] = [0, 0];
456
+ }
457
+ const count = flow.nodes.filter((n) => n.type === type).length + 1;
458
+ const node = { id: `${type}-${Date.now().toString(36)}${++nextId}`, type, label: `${this.typeLabel(type)} ${count}`, x: snap(x, grid), y: snap(y, grid) };
459
+ const edges = source && canConnect({ ...flow, nodes: [...flow.nodes, node] }, source.id, node.id) ? [...flow.edges, this.newEdge(source.id, node.id)] : flow.edges;
460
+ this.flow.set({ nodes: [...flow.nodes, node], edges });
461
+ this.announce(cbFormat(this.t().flowAdded, cbFormat(this.t().flowNodeOf, this.typeLabel(type), node.label)));
462
+ this.selected.set(node.id);
463
+ afterNextRender(() => {
464
+ if (this.view() === 'list') {
465
+ const input = this.host.nativeElement.querySelector(`[data-name="${node.id}"]`);
466
+ input?.focus();
467
+ input?.select();
468
+ }
469
+ else {
470
+ this.ensureVisible(node);
471
+ this.startEdit(node.id, 'node');
472
+ }
473
+ }, { injector: this.injector });
474
+ return node;
475
+ }
476
+ /** Conecta dos nodos (si se puede: no consigo mismo, no repetida, no hacia un inicio ni desde un fin). */
477
+ connect(from, to, label) {
478
+ const flow = this.flow();
479
+ if (!canConnect(flow, from, to)) {
480
+ this.announce(cbFormat(this.t().flowCannotConnect, this.nameOf(from), this.nameOf(to)));
481
+ return null;
482
+ }
483
+ const edge = this.newEdge(from, to, label);
484
+ this.flow.set({ ...flow, edges: [...flow.edges, edge] });
485
+ this.announce(cbFormat(this.t().flowConnected, this.nameOf(from), this.nameOf(to)));
486
+ return edge;
487
+ }
488
+ removeNode(id) {
489
+ const flow = this.flow();
490
+ const node = flow.nodes.find((n) => n.id === id);
491
+ if (!node)
492
+ return;
493
+ const order = readingOrder(flow);
494
+ const next = order[order.findIndex((n) => n.id === id) + 1] ?? order[order.findIndex((n) => n.id === id) - 1];
495
+ this.flow.set({ nodes: flow.nodes.filter((n) => n.id !== id), edges: flow.edges.filter((e) => e.from !== id && e.to !== id) });
496
+ if (this.connectFrom() === id)
497
+ this.connectFrom.set(null);
498
+ this.announce(cbFormat(this.t().flowRemoved, cbFormat(this.t().flowNodeOf, this.typeLabel(node.type), node.label)));
499
+ this.focusAfterRemoval(next && next.id !== id ? next.id : null);
500
+ }
501
+ removeEdge(id) {
502
+ const flow = this.flow();
503
+ const edge = flow.edges.find((e) => e.id === id);
504
+ if (!edge)
505
+ return;
506
+ this.flow.set({ ...flow, edges: flow.edges.filter((e) => e.id !== id) });
507
+ this.announce(cbFormat(this.t().flowRemoved, cbFormat(this.t().flowEdgeOf, this.nameOf(edge.from), this.nameOf(edge.to))));
508
+ this.focusAfterRemoval(edge.from);
509
+ }
510
+ rename(id, label) {
511
+ const clean = label.trim();
512
+ if (!clean)
513
+ return;
514
+ this.flow.update((f) => ({ ...f, nodes: f.nodes.map((n) => (n.id === id ? { ...n, label: clean } : n)) }));
515
+ }
516
+ relabel(id, label) {
517
+ const clean = label.trim();
518
+ this.flow.update((f) => ({ ...f, edges: f.edges.map((e) => (e.id === id ? { ...e, label: clean || undefined } : e)) }));
519
+ }
520
+ /** Coloca los nodos por niveles de arriba abajo y ajusta la vista. */
521
+ layout() {
522
+ this.flow.set(cbLayoutFlow(this.flow(), this.gridSize()));
523
+ this.announce(this.t().flowLaidOut);
524
+ afterNextRender(() => this.fit(), { injector: this.injector });
525
+ }
526
+ /** Ajusta zoom y desplazamiento para ver todo el flujo. */
527
+ fit() {
528
+ const el = this.viewport()?.nativeElement;
529
+ const b = flowBounds(this.flow().nodes);
530
+ if (!el || !b || !el.clientWidth)
531
+ return;
532
+ const pad = 48;
533
+ const zoom = clamp(Math.min((el.clientWidth - pad * 2) / b.w, (el.clientHeight - pad * 2) / b.h, 1.25), MIN_ZOOM, MAX_ZOOM);
534
+ this.zoom.set(round2(zoom));
535
+ this.pan.set({ x: (el.clientWidth - b.w * zoom) / 2 - b.x * zoom, y: (el.clientHeight - b.h * zoom) / 2 - b.y * zoom });
536
+ }
537
+ /** Multiplica el zoom por `factor` alrededor del centro de la vista (o del punto `at`, en px de la vista). */
538
+ zoomBy(factor, at) {
539
+ const el = this.viewport()?.nativeElement;
540
+ const z1 = this.zoom();
541
+ const z2 = round2(clamp(z1 * factor, MIN_ZOOM, MAX_ZOOM));
542
+ if (z2 === z1)
543
+ return;
544
+ const p = at ?? { x: (el?.clientWidth ?? 0) / 2, y: (el?.clientHeight ?? 0) / 2 };
545
+ const { x, y } = this.pan();
546
+ this.pan.set({ x: p.x - ((p.x - x) * z2) / z1, y: p.y - ((p.y - y) * z2) / z1 });
547
+ this.zoom.set(z2);
548
+ }
549
+ // --- Barra -------------------------------------------------------------------
550
+ toggleConnect() {
551
+ const node = this.selectedNode();
552
+ if (this.connectFrom())
553
+ this.cancelConnect();
554
+ else if (node)
555
+ this.beginConnect(node);
556
+ }
557
+ deleteSelected() {
558
+ const id = this.selected();
559
+ if (!id)
560
+ return;
561
+ if (this.flow().nodes.some((n) => n.id === id))
562
+ this.removeNode(id);
563
+ else
564
+ this.removeEdge(id);
565
+ }
566
+ connectFromList(from, select) {
567
+ if (!select.value)
568
+ return;
569
+ if (this.connect(from, select.value))
570
+ select.value = '';
571
+ }
572
+ reveal(id) {
573
+ this.selected.set(id);
574
+ afterNextRender(() => {
575
+ if (this.view() === 'list') {
576
+ this.host.nativeElement.querySelector(`[data-name="${id}"]`)?.focus();
577
+ }
578
+ else {
579
+ const node = this.nodeMap().get(id);
580
+ if (node)
581
+ this.ensureVisible(node);
582
+ this.focusNode(id);
583
+ }
584
+ }, { injector: this.injector });
585
+ }
586
+ // --- Teclado ---------------------------------------------------------------------
587
+ onNodeKeydown(event, rendered) {
588
+ if (event.target !== event.currentTarget || event.ctrlKey || event.metaKey || event.altKey)
589
+ return;
590
+ // La de la plantilla puede ser de antes del último repintado (tecla mantenida).
591
+ const node = this.flow().nodes.find((n) => n.id === rendered.id) ?? rendered;
592
+ const grid = Math.max(1, this.gridSize());
593
+ const step = event.shiftKey ? grid * 4 : grid;
594
+ const arrows = { ArrowLeft: [-step, 0], ArrowRight: [step, 0], ArrowUp: [0, -step], ArrowDown: [0, step] };
595
+ const key = event.key;
596
+ if (key === 'Enter' || key === ' ' || key === 'F2') {
597
+ const from = this.connectFrom();
598
+ if (from && from !== node.id && key !== 'F2') {
599
+ this.connect(from, node.id);
600
+ this.connectFrom.set(null);
601
+ }
602
+ else if (this.readonly()) {
603
+ if (key !== 'F2')
604
+ this.nodeOpen.emit(node);
605
+ }
606
+ else {
607
+ this.startEdit(node.id, 'node');
608
+ }
609
+ }
610
+ else if (this.readonly()) {
611
+ return;
612
+ }
613
+ else if (arrows[key]) {
614
+ const [dx, dy] = arrows[key];
615
+ this.moveNode(node.id, snap(node.x + dx, grid), snap(node.y + dy, grid));
616
+ afterNextRender(() => this.ensureVisible(this.nodeMap().get(node.id) ?? node), { injector: this.injector });
617
+ }
618
+ else if (key === 'Delete' || key === 'Backspace') {
619
+ this.removeNode(node.id);
620
+ }
621
+ else if ((key === 'c' || key === 'C') && node.type !== 'end') {
622
+ if (this.connectFrom() === node.id)
623
+ this.cancelConnect();
624
+ else
625
+ this.beginConnect(node);
626
+ }
627
+ else {
628
+ return;
629
+ }
630
+ event.preventDefault();
631
+ event.stopPropagation();
632
+ }
633
+ onEdgeKeydown(event, edge) {
634
+ if (this.readonly() || event.ctrlKey || event.metaKey || event.altKey)
635
+ return;
636
+ if (event.key === 'Enter' || event.key === ' ' || event.key === 'F2')
637
+ this.startEdit(edge.id, 'edge');
638
+ else if (event.key === 'Delete' || event.key === 'Backspace')
639
+ this.removeEdge(edge.id);
640
+ else
641
+ return;
642
+ event.preventDefault();
643
+ event.stopPropagation();
644
+ }
645
+ onHostKeydown(event) {
646
+ const target = event.target;
647
+ if (/^(INPUT|TEXTAREA|SELECT)$/.test(target.tagName) || event.ctrlKey || event.metaKey || event.altKey)
648
+ return;
649
+ if (event.key === 'Escape' && this.connectFrom()) {
650
+ this.cancelConnect();
651
+ }
652
+ else if (this.view() === 'diagram' && (event.key === '+' || event.key === '=')) {
653
+ this.zoomBy(ZOOM_STEP);
654
+ }
655
+ else if (this.view() === 'diagram' && event.key === '-') {
656
+ this.zoomBy(1 / ZOOM_STEP);
657
+ }
658
+ else if (this.view() === 'diagram' && event.key === '0') {
659
+ this.fit();
660
+ }
661
+ else {
662
+ return;
663
+ }
664
+ event.preventDefault();
665
+ }
666
+ // --- Edición en línea ----------------------------------------------------------------
667
+ startEdit(id, kind) {
668
+ if (this.readonly())
669
+ return;
670
+ this.editing.set({ id, kind });
671
+ afterNextRender(() => {
672
+ const input = this.editor()?.nativeElement;
673
+ if (!input)
674
+ return;
675
+ const flow = this.flow();
676
+ input.value = kind === 'node' ? (flow.nodes.find((n) => n.id === id)?.label ?? '') : (flow.edges.find((e) => e.id === id)?.label ?? '');
677
+ input.focus();
678
+ input.select();
679
+ }, { injector: this.injector });
680
+ }
681
+ onEditorKeydown(event) {
682
+ event.stopPropagation();
683
+ if (event.key === 'Enter') {
684
+ event.preventDefault();
685
+ this.commitEdit(true);
686
+ }
687
+ else if (event.key === 'Escape') {
688
+ event.preventDefault();
689
+ this.cancelEdit();
690
+ }
691
+ }
692
+ /** Guarda lo escrito (al pulsar Intro o al salir del campo). */
693
+ commitEdit(refocus = false) {
694
+ const editing = this.editing();
695
+ const input = this.editor()?.nativeElement;
696
+ if (!editing || !input)
697
+ return;
698
+ this.editing.set(null);
699
+ if (editing.kind === 'node')
700
+ this.rename(editing.id, input.value);
701
+ else
702
+ this.relabel(editing.id, input.value);
703
+ if (refocus)
704
+ this.focusItem(editing);
705
+ }
706
+ cancelEdit() {
707
+ const editing = this.editing();
708
+ this.editing.set(null);
709
+ if (editing)
710
+ this.focusItem(editing);
711
+ }
712
+ focusItem(editing) {
713
+ afterNextRender(() => {
714
+ const selector = editing.kind === 'node' ? `[data-node="${editing.id}"]` : `[data-edge="${editing.id}"]`;
715
+ this.host.nativeElement.querySelector(selector)?.focus();
716
+ }, { injector: this.injector });
717
+ }
718
+ // --- Puntero ----------------------------------------------------------------------
719
+ onNodePointerDown(event, node) {
720
+ if (event.button !== 0)
721
+ return;
722
+ event.stopPropagation();
723
+ const el = event.currentTarget;
724
+ const from = this.connectFrom();
725
+ if (from && from !== node.id) {
726
+ event.preventDefault();
727
+ this.connect(from, node.id);
728
+ this.connectFrom.set(null);
729
+ el.focus();
730
+ return;
731
+ }
732
+ if (this.editing())
733
+ this.commitEdit();
734
+ this.selected.set(node.id);
735
+ if (this.readonly())
736
+ return;
737
+ // Sin el default no se selecciona texto al arrastrar; el foco se da a mano.
738
+ event.preventDefault();
739
+ el.focus({ preventScroll: true });
740
+ const start = { x: event.clientX, y: event.clientY, nx: node.x, ny: node.y };
741
+ const zoom = this.zoom();
742
+ const grid = this.gridSize();
743
+ let moved = false;
744
+ this.track(event, el, (e) => {
745
+ const [dx, dy] = [(e.clientX - start.x) / zoom, (e.clientY - start.y) / zoom];
746
+ if (!moved && Math.hypot(dx, dy) * zoom < 3)
747
+ return;
748
+ moved = true;
749
+ this.dragPos.set({ id: node.id, x: start.nx + dx, y: start.ny + dy });
750
+ }, () => {
751
+ const pos = this.dragPos();
752
+ this.dragPos.set(null);
753
+ if (moved && pos)
754
+ this.moveNode(node.id, snap(pos.x, grid), snap(pos.y, grid));
755
+ });
756
+ }
757
+ onNodeDblClick(node) {
758
+ if (this.readonly())
759
+ this.nodeOpen.emit(node);
760
+ else
761
+ this.startEdit(node.id, 'node');
762
+ }
763
+ /** Arrastrar desde el tirador crea una conexión con el nodo donde se suelta. */
764
+ onHandlePointerDown(event, node) {
765
+ if (event.button !== 0 || this.readonly())
766
+ return;
767
+ event.stopPropagation();
768
+ event.preventDefault();
769
+ const el = event.currentTarget;
770
+ this.selected.set(node.id);
771
+ this.linkDrag.set({ from: node.id, ...this.toWorld(event) });
772
+ this.track(event, el, (e) => this.linkDrag.set({ from: node.id, ...this.toWorld(e) }), (e) => {
773
+ this.linkDrag.set(null);
774
+ const target = this.host.nativeElement.ownerDocument.elementFromPoint?.(e.clientX, e.clientY)?.closest('[data-node]');
775
+ const to = target?.dataset['node'];
776
+ if (to && to !== node.id)
777
+ this.connect(node.id, to);
778
+ });
779
+ }
780
+ onEdgePointerDown(event, edge) {
781
+ if (event.button !== 0)
782
+ return;
783
+ event.stopPropagation();
784
+ if (this.editing())
785
+ this.commitEdit();
786
+ this.selected.set(edge.id);
787
+ const g = this.host.nativeElement.querySelector(`[data-edge="${edge.id}"]`);
788
+ event.preventDefault();
789
+ g?.focus({ preventScroll: true });
790
+ }
791
+ /** Arrastrar el fondo desplaza el lienzo; un clic sin moverse quita la selección. */
792
+ onBackgroundPointerDown(event) {
793
+ if (event.button !== 0 && event.button !== 1)
794
+ return;
795
+ if (this.editing())
796
+ this.commitEdit();
797
+ const el = event.currentTarget;
798
+ const pointer = { x: event.clientX, y: event.clientY };
799
+ const origin = this.pan();
800
+ let moved = false;
801
+ this.track(event, el, (e) => {
802
+ const [dx, dy] = [e.clientX - pointer.x, e.clientY - pointer.y];
803
+ if (!moved && Math.hypot(dx, dy) < 3)
804
+ return;
805
+ moved = true;
806
+ this.panning.set(true);
807
+ this.pan.set({ x: origin.x + dx, y: origin.y + dy });
808
+ }, () => {
809
+ this.panning.set(false);
810
+ if (!moved) {
811
+ if (this.connectFrom())
812
+ this.cancelConnect();
813
+ this.selected.set(null);
814
+ }
815
+ });
816
+ }
817
+ /** Rueda: desplaza; con Ctrl (o el gesto de pellizcar del panel táctil), amplía alrededor del puntero. */
818
+ onWheel(event) {
819
+ event.preventDefault();
820
+ if (event.ctrlKey || event.metaKey) {
821
+ const rect = event.currentTarget.getBoundingClientRect();
822
+ this.zoomBy(event.deltaY < 0 ? 1.1 : 1 / 1.1, { x: event.clientX - rect.left, y: event.clientY - rect.top });
823
+ }
824
+ else {
825
+ const { x, y } = this.pan();
826
+ this.pan.set({ x: x - event.deltaX, y: y - event.deltaY });
827
+ }
828
+ }
829
+ track(event, el, move, end) {
830
+ el.setPointerCapture?.(event.pointerId);
831
+ const up = (e) => {
832
+ el.removeEventListener('pointermove', move);
833
+ el.removeEventListener('pointerup', up);
834
+ el.removeEventListener('pointercancel', up);
835
+ end(e);
836
+ };
837
+ el.addEventListener('pointermove', move);
838
+ el.addEventListener('pointerup', up);
839
+ el.addEventListener('pointercancel', up);
840
+ }
841
+ // --- Utilidades ---------------------------------------------------------------------
842
+ beginConnect(node) {
843
+ this.connectFrom.set(node.id);
844
+ this.announce(cbFormat(this.t().flowConnectFrom, node.label));
845
+ }
846
+ cancelConnect() {
847
+ this.connectFrom.set(null);
848
+ this.announce(this.t().flowConnectCancelled);
849
+ }
850
+ moveNode(id, x, y) {
851
+ this.flow.update((f) => ({ ...f, nodes: f.nodes.map((n) => (n.id === id ? { ...n, x, y } : n)) }));
852
+ }
853
+ newEdge(from, to, label) {
854
+ return { id: `edge-${Date.now().toString(36)}${++nextId}`, from, to, ...(label ? { label } : {}) };
855
+ }
856
+ /** Coordenadas del lienzo bajo el puntero. */
857
+ toWorld(event) {
858
+ const rect = this.viewport()?.nativeElement.getBoundingClientRect();
859
+ const { x, y } = this.pan();
860
+ const zoom = this.zoom();
861
+ return { x: (event.clientX - (rect?.left ?? 0) - x) / zoom, y: (event.clientY - (rect?.top ?? 0) - y) / zoom };
862
+ }
863
+ /** Desplaza el lienzo lo justo para que se vea el nodo. */
864
+ ensureVisible(node) {
865
+ const el = this.viewport()?.nativeElement;
866
+ if (!el || !el.clientWidth)
867
+ return;
868
+ const { w, h } = CB_FLOW_NODE_SIZE[node.type];
869
+ const zoom = this.zoom();
870
+ const margin = 24;
871
+ let { x, y } = this.pan();
872
+ const [left, top] = [node.x * zoom + x, node.y * zoom + y];
873
+ const [right, bottom] = [left + w * zoom, top + h * zoom];
874
+ if (left < margin)
875
+ x += margin - left;
876
+ else if (right > el.clientWidth - margin)
877
+ x -= right - (el.clientWidth - margin);
878
+ if (top < margin)
879
+ y += margin - top;
880
+ else if (bottom > el.clientHeight - margin)
881
+ y -= bottom - (el.clientHeight - margin);
882
+ this.pan.set({ x, y });
883
+ }
884
+ focusNode(id) {
885
+ this.host.nativeElement.querySelector(`[data-node="${id}"]`)?.focus();
886
+ }
887
+ focusAfterRemoval(nextNode) {
888
+ const hadFocus = this.host.nativeElement.contains(this.host.nativeElement.ownerDocument.activeElement);
889
+ if (!hadFocus)
890
+ return;
891
+ afterNextRender(() => {
892
+ if (this.view() === 'list') {
893
+ const target = nextNode && this.host.nativeElement.querySelector(`[data-name="${nextNode}"]`);
894
+ (target || this.host.nativeElement.querySelector('.cb-flow__view[aria-pressed="true"]'))?.focus();
895
+ }
896
+ else if (nextNode && this.flow().nodes.some((n) => n.id === nextNode)) {
897
+ this.focusNode(nextNode);
898
+ }
899
+ else {
900
+ this.viewport()?.nativeElement.focus();
901
+ }
902
+ }, { injector: this.injector });
903
+ }
904
+ nodeLabel(n) {
905
+ const t = this.t();
906
+ const exits = (this.outgoing().get(n.id) ?? []).map((e) => (e.label ? `${this.nameOf(e.to)} (${e.label})` : this.nameOf(e.to)));
907
+ const parts = [cbFormat(t.flowNodeOf, this.typeLabel(n.type), n.label)];
908
+ if (n.type !== 'end')
909
+ parts.push(exits.length ? cbFormat(t.flowExits, exits.join(', ')) : t.flowNoExits);
910
+ for (const issue of this.issuesByNode().get(n.id) ?? [])
911
+ parts.push(cbFormat(t.flowIssueOf, this.issueText(issue)));
912
+ return parts.join('. ');
913
+ }
914
+ typeLabel(type) {
915
+ const t = this.t();
916
+ return { start: t.flowStart, task: t.flowTask, decision: t.flowDecision, end: t.flowEnd }[type];
917
+ }
918
+ nameOf(id) {
919
+ return this.flow().nodes.find((n) => n.id === id)?.label ?? id;
920
+ }
921
+ /** Destinos posibles en la lista: todos menos él mismo, los inicios y los ya conectados. */
922
+ targetsFor(node) {
923
+ const flow = this.flow();
924
+ return this.ordered().filter((o) => canConnect(flow, node.id, o.id));
925
+ }
926
+ issueText(issue) {
927
+ const t = this.t();
928
+ const name = issue.nodeId ? this.nameOf(issue.nodeId) : '';
929
+ const text = {
930
+ 'no-start': t.flowIssueNoStart,
931
+ 'many-starts': t.flowIssueManyStarts,
932
+ 'no-end': t.flowIssueNoEnd,
933
+ 'start-incoming': t.flowIssueStartIncoming,
934
+ 'end-outgoing': t.flowIssueEndOutgoing,
935
+ 'dead-end': t.flowIssueDeadEnd,
936
+ 'decision-branches': t.flowIssueBranches,
937
+ 'branch-label': t.flowIssueBranchLabel,
938
+ unreachable: t.flowIssueUnreachable,
939
+ };
940
+ return cbFormat(text[issue.code], name);
941
+ }
942
+ diamond(type) {
943
+ const { w, h } = CB_FLOW_NODE_SIZE[type];
944
+ return `${w / 2},1 ${w - 1},${h / 2} ${w / 2},${h - 1} 1,${h / 2}`;
945
+ }
946
+ announce(text) {
947
+ // Un espacio duro alterno hace que se repita aunque el texto sea el mismo.
948
+ this.announceFlip = !this.announceFlip;
949
+ this.announcement.set(this.announceFlip ? text : `${text} `);
950
+ }
951
+ static { this.ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbFlowEditor, deps: [], target: i0.ɵɵFactoryTarget.Component }); }
952
+ static { this.ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "22.0.5", type: CbFlowEditor, isStandalone: true, selector: "cb-flow-editor", inputs: { flow: { classPropertyName: "flow", publicName: "flow", isSignal: true, isRequired: false, transformFunction: null }, readonly: { classPropertyName: "readonly", publicName: "readonly", isSignal: true, isRequired: false, transformFunction: null }, gridSize: { classPropertyName: "gridSize", publicName: "gridSize", isSignal: true, isRequired: false, transformFunction: null }, zoom: { classPropertyName: "zoom", publicName: "zoom", isSignal: true, isRequired: false, transformFunction: null }, view: { classPropertyName: "view", publicName: "view", isSignal: true, isRequired: false, transformFunction: null }, selected: { classPropertyName: "selected", publicName: "selected", isSignal: true, isRequired: false, transformFunction: null }, showValidation: { classPropertyName: "showValidation", publicName: "showValidation", isSignal: true, isRequired: false, transformFunction: null }, fitOnLoad: { classPropertyName: "fitOnLoad", publicName: "fitOnLoad", isSignal: true, isRequired: false, transformFunction: null }, ariaLabel: { classPropertyName: "ariaLabel", publicName: "ariaLabel", isSignal: true, isRequired: false, transformFunction: null } }, outputs: { flow: "flowChange", zoom: "zoomChange", view: "viewChange", selected: "selectedChange", nodeOpen: "nodeOpen" }, host: { attributes: { "role": "group" }, listeners: { "keydown": "onHostKeydown($event)" }, properties: { "attr.aria-label": "ariaLabel() || t().flowEditor" }, classAttribute: "cb-flow" }, viewQueries: [{ propertyName: "viewport", first: true, predicate: ["viewport"], descendants: true, isSignal: true }, { propertyName: "editor", first: true, predicate: ["editor"], descendants: true, isSignal: true }], ngImport: i0, template: `
953
+ <div class="cb-flow__bar">
954
+ @if (!readonly()) {
955
+ <div class="cb-flow__group" role="group" [attr.aria-label]="t().flowAdd">
956
+ @for (type of types; track type) {
957
+ <button type="button" class="cb-flow__btn" (click)="addNode(type)">
958
+ <span class="cb-flow__swatch" [class]="'cb-flow__swatch cb-flow__swatch--' + type" aria-hidden="true"></span>
959
+ {{ typeLabel(type) }}
960
+ </button>
961
+ }
962
+ </div>
963
+ <div class="cb-flow__group">
964
+ <button
965
+ type="button"
966
+ class="cb-flow__btn"
967
+ [attr.aria-pressed]="!!connectFrom()"
968
+ [disabled]="!selectedNode() || selectedNode()!.type === 'end'"
969
+ (click)="toggleConnect()"
970
+ >
971
+ <cb-icon name="spline" [size]="16" aria-hidden="true" />{{ t().flowConnect }}
972
+ </button>
973
+ <button type="button" class="cb-flow__btn" [disabled]="!selected() || !selectedExists()" (click)="deleteSelected()">
974
+ <cb-icon name="trash" [size]="16" aria-hidden="true" />{{ t().flowDelete }}
975
+ </button>
976
+ <button type="button" class="cb-flow__btn" [disabled]="!flow().nodes.length" (click)="layout()">
977
+ <cb-icon name="workflow" [size]="16" aria-hidden="true" />{{ t().flowLayout }}
978
+ </button>
979
+ </div>
980
+ }
981
+ @if (view() === 'diagram') {
982
+ <div class="cb-flow__group" role="group" [attr.aria-label]="t().flowZoom">
983
+ <button type="button" class="cb-flow__icon" [attr.aria-label]="t().flowZoomOut" [disabled]="zoom() <= minZoom" (click)="zoomBy(1 / step)">
984
+ <cb-icon name="zoom-out" [size]="18" aria-hidden="true" />
985
+ </button>
986
+ <span class="cb-flow__percent" aria-hidden="true">{{ percent() }}</span>
987
+ <button type="button" class="cb-flow__icon" [attr.aria-label]="t().flowZoomIn" [disabled]="zoom() >= maxZoom" (click)="zoomBy(step)">
988
+ <cb-icon name="zoom-in" [size]="18" aria-hidden="true" />
989
+ </button>
990
+ <button type="button" class="cb-flow__icon" [attr.aria-label]="t().flowFit" [attr.title]="t().flowFit" [disabled]="!flow().nodes.length" (click)="fit()">
991
+ <cb-icon name="scan" [size]="18" aria-hidden="true" />
992
+ </button>
993
+ </div>
994
+ }
995
+ <span class="cb-flow__spacer"></span>
996
+ <div class="cb-flow__views" role="group" [attr.aria-label]="t().flowView">
997
+ <button type="button" class="cb-flow__view" [attr.aria-pressed]="view() === 'diagram'" (click)="view.set('diagram')">{{ t().flowDiagram }}</button>
998
+ <button type="button" class="cb-flow__view" [attr.aria-pressed]="view() === 'list'" (click)="view.set('list')">{{ t().flowList }}</button>
999
+ </div>
1000
+ </div>
1001
+
1002
+ @if (view() === 'diagram') {
1003
+ <div
1004
+ #viewport
1005
+ class="cb-flow__viewport"
1006
+ tabindex="-1"
1007
+ [class.cb-flow__viewport--connect]="!!connectFrom()"
1008
+ [class.cb-flow__viewport--panning]="panning()"
1009
+ [style.background-size]="gridBackground().size"
1010
+ [style.background-position]="gridBackground().position"
1011
+ (pointerdown)="onBackgroundPointerDown($event)"
1012
+ (wheel)="onWheel($event)"
1013
+ >
1014
+ @if (!flow().nodes.length) {
1015
+ <p class="cb-flow__empty">{{ t().flowEmpty }}</p>
1016
+ }
1017
+ <div class="cb-flow__world" [style.transform]="worldTransform()">
1018
+ @for (n of nodes(); track n.id) {
1019
+ <div
1020
+ class="cb-flow__node"
1021
+ [class]="'cb-flow__node cb-flow__node--' + n.type"
1022
+ [class.cb-is-selected]="selected() === n.id"
1023
+ [class.cb-is-source]="connectFrom() === n.id"
1024
+ [class.cb-flow__node--issue]="issuesByNode().has(n.id)"
1025
+ [attr.data-node]="n.id"
1026
+ tabindex="0"
1027
+ role="button"
1028
+ [attr.aria-label]="nodeLabel(n)"
1029
+ [attr.aria-describedby]="uid + (readonly() ? '-ro-hint' : '-node-hint')"
1030
+ [style.left.px]="n.x"
1031
+ [style.top.px]="n.y"
1032
+ [style.width.px]="sizes[n.type].w"
1033
+ [style.height.px]="sizes[n.type].h"
1034
+ (pointerdown)="onNodePointerDown($event, n)"
1035
+ (dblclick)="onNodeDblClick(n)"
1036
+ (keydown)="onNodeKeydown($event, n)"
1037
+ (focus)="selected.set(n.id)"
1038
+ >
1039
+ <svg class="cb-flow__shape" [attr.viewBox]="'0 0 ' + sizes[n.type].w + ' ' + sizes[n.type].h" aria-hidden="true">
1040
+ @switch (n.type) {
1041
+ @case ('decision') {
1042
+ <polygon [attr.points]="diamond(n.type)" />
1043
+ }
1044
+ @case ('task') {
1045
+ <rect x="1" y="1" rx="8" [attr.width]="sizes[n.type].w - 2" [attr.height]="sizes[n.type].h - 2" />
1046
+ }
1047
+ @default {
1048
+ <rect x="1" y="1" [attr.rx]="(sizes[n.type].h - 2) / 2" [attr.width]="sizes[n.type].w - 2" [attr.height]="sizes[n.type].h - 2" />
1049
+ }
1050
+ }
1051
+ </svg>
1052
+ <span class="cb-flow__label">{{ n.label }}</span>
1053
+ @if (issuesByNode().has(n.id)) {
1054
+ <span class="cb-flow__badge" aria-hidden="true">!</span>
1055
+ }
1056
+ @if (!readonly() && n.type !== 'end') {
1057
+ <span class="cb-flow__handle" aria-hidden="true" (pointerdown)="onHandlePointerDown($event, n)"></span>
1058
+ }
1059
+ </div>
1060
+ }
1061
+ <svg
1062
+ class="cb-flow__edges"
1063
+ [attr.viewBox]="svgBox().x + ' ' + svgBox().y + ' ' + svgBox().w + ' ' + svgBox().h"
1064
+ [style.left.px]="svgBox().x"
1065
+ [style.top.px]="svgBox().y"
1066
+ [style.width.px]="svgBox().w"
1067
+ [style.height.px]="svgBox().h"
1068
+ >
1069
+ @for (e of edgeViews(); track e.edge.id) {
1070
+ <g
1071
+ class="cb-flow__edge"
1072
+ [class.cb-is-selected]="selected() === e.edge.id"
1073
+ [attr.data-edge]="e.edge.id"
1074
+ tabindex="0"
1075
+ role="button"
1076
+ [attr.aria-label]="e.label"
1077
+ [attr.aria-describedby]="readonly() ? null : uid + '-edge-hint'"
1078
+ (pointerdown)="onEdgePointerDown($event, e.edge)"
1079
+ (dblclick)="startEdit(e.edge.id, 'edge')"
1080
+ (keydown)="onEdgeKeydown($event, e.edge)"
1081
+ (focus)="selected.set(e.edge.id)"
1082
+ >
1083
+ <path class="cb-flow__edge-hit" [attr.d]="e.d" />
1084
+ <path class="cb-flow__edge-line" [attr.d]="e.d" />
1085
+ <polygon class="cb-flow__edge-arrow" [attr.points]="e.arrow" />
1086
+ </g>
1087
+ }
1088
+ @if (tempLine(); as line) {
1089
+ <path class="cb-flow__edge-line cb-flow__edge-line--temp" [attr.d]="line" />
1090
+ }
1091
+ </svg>
1092
+ @for (e of edgeViews(); track e.edge.id) {
1093
+ @if (e.edge.label) {
1094
+ <span
1095
+ class="cb-flow__edge-label"
1096
+ [class.cb-is-selected]="selected() === e.edge.id"
1097
+ aria-hidden="true"
1098
+ [style.left.px]="e.mid[0]"
1099
+ [style.top.px]="e.mid[1]"
1100
+ (pointerdown)="onEdgePointerDown($event, e.edge)"
1101
+ (dblclick)="startEdit(e.edge.id, 'edge')"
1102
+ >{{ e.edge.label }}</span
1103
+ >
1104
+ }
1105
+ }
1106
+ @if (editorBox(); as box) {
1107
+ <input
1108
+ #editor
1109
+ class="cb-flow__editor"
1110
+ [attr.aria-label]="box.label"
1111
+ [style.left.px]="box.x"
1112
+ [style.top.px]="box.y"
1113
+ [style.width.px]="box.w"
1114
+ (keydown)="onEditorKeydown($event)"
1115
+ (blur)="commitEdit()"
1116
+ (pointerdown)="$event.stopPropagation()"
1117
+ />
1118
+ }
1119
+ </div>
1120
+ </div>
1121
+ } @else {
1122
+ <ol class="cb-flow__list" [attr.aria-label]="t().flowList">
1123
+ @if (!flow().nodes.length) {
1124
+ <li class="cb-flow__empty cb-flow__empty--list">{{ t().flowEmpty }}</li>
1125
+ }
1126
+ @for (n of ordered(); track n.id) {
1127
+ <li class="cb-flow__item" [class.cb-flow__item--issue]="issuesByNode().has(n.id)">
1128
+ <div class="cb-flow__item-head">
1129
+ <span class="cb-flow__type" [class]="'cb-flow__type cb-flow__type--' + n.type">{{ typeLabel(n.type) }}</span>
1130
+ @if (readonly()) {
1131
+ <strong class="cb-flow__item-name">{{ n.label }}</strong>
1132
+ } @else {
1133
+ <input
1134
+ class="cb-flow__field cb-flow__item-name"
1135
+ [attr.data-name]="n.id"
1136
+ [value]="n.label"
1137
+ [attr.aria-label]="format(t().flowNameOf, typeLabel(n.type))"
1138
+ (change)="rename(n.id, $any($event.target).value)"
1139
+ />
1140
+ <button type="button" class="cb-flow__icon" [attr.aria-label]="format(t().flowDeleteNode, n.label)" (click)="removeNode(n.id)">
1141
+ <cb-icon name="trash" [size]="16" aria-hidden="true" />
1142
+ </button>
1143
+ }
1144
+ </div>
1145
+ @for (issue of issuesByNode().get(n.id) ?? []; track $index) {
1146
+ <p class="cb-flow__item-issue">{{ issueText(issue) }}</p>
1147
+ }
1148
+ <ul class="cb-flow__exits" [attr.aria-label]="format(t().flowExitsOf, n.label)">
1149
+ @for (e of outgoing().get(n.id) ?? []; track e.id) {
1150
+ <li class="cb-flow__exit">
1151
+ <span aria-hidden="true">→</span>
1152
+ <span class="cb-flow__exit-target">{{ nameOf(e.to) }}</span>
1153
+ @if (readonly()) {
1154
+ @if (e.label) {
1155
+ <span class="cb-flow__exit-label">{{ e.label }}</span>
1156
+ }
1157
+ } @else {
1158
+ <input
1159
+ class="cb-flow__field cb-flow__field--sm"
1160
+ [value]="e.label ?? ''"
1161
+ [placeholder]="t().flowEdgeLabel"
1162
+ [attr.aria-label]="format(t().flowEdgeLabelOf, n.label, nameOf(e.to))"
1163
+ (change)="relabel(e.id, $any($event.target).value)"
1164
+ />
1165
+ <button type="button" class="cb-flow__icon" [attr.aria-label]="format(t().flowRemoveConnection, nameOf(e.to))" (click)="removeEdge(e.id)">
1166
+ <cb-icon name="x" [size]="16" aria-hidden="true" />
1167
+ </button>
1168
+ }
1169
+ </li>
1170
+ } @empty {
1171
+ <li class="cb-flow__exit cb-flow__exit--none">{{ t().flowNoExits }}</li>
1172
+ }
1173
+ </ul>
1174
+ @if (!readonly() && n.type !== 'end' && targetsFor(n).length) {
1175
+ <div class="cb-flow__connect-row">
1176
+ <select #target class="cb-flow__field" [attr.aria-label]="format(t().flowConnectTo, n.label)">
1177
+ <option value="">{{ t().flowChooseTarget }}</option>
1178
+ @for (o of targetsFor(n); track o.id) {
1179
+ <option [value]="o.id">{{ typeLabel(o.type) }}: {{ o.label }}</option>
1180
+ }
1181
+ </select>
1182
+ <button type="button" class="cb-flow__btn" (click)="connectFromList(n.id, target)">{{ t().flowAddConnection }}</button>
1183
+ </div>
1184
+ }
1185
+ </li>
1186
+ }
1187
+ </ol>
1188
+ }
1189
+
1190
+ @if (showValidation() && flow().nodes.length) {
1191
+ @if (issues().length) {
1192
+ <details class="cb-flow__issues" open>
1193
+ <summary>{{ format(t().flowIssues, issues().length) }}</summary>
1194
+ <ul class="cb-flow__issues-list">
1195
+ @for (issue of issues(); track $index) {
1196
+ <li [class.cb-flow__issue--warning]="issue.severity === 'warning'">
1197
+ @if (issue.nodeId) {
1198
+ <button type="button" class="cb-flow__issue-link" (click)="reveal(issue.nodeId)">{{ issueText(issue) }}</button>
1199
+ } @else {
1200
+ {{ issueText(issue) }}
1201
+ }
1202
+ </li>
1203
+ }
1204
+ </ul>
1205
+ </details>
1206
+ } @else {
1207
+ <p class="cb-flow__valid">
1208
+ <cb-icon name="check" [size]="16" aria-hidden="true" />{{ t().flowNoIssues }}
1209
+ </p>
1210
+ }
1211
+ }
1212
+
1213
+ <span class="cb-flow__sr" [id]="uid + '-node-hint'">{{ t().flowNodeHint }}</span>
1214
+ <span class="cb-flow__sr" [id]="uid + '-ro-hint'">{{ t().flowReadonlyHint }}</span>
1215
+ <span class="cb-flow__sr" [id]="uid + '-edge-hint'">{{ t().flowEdgeHint }}</span>
1216
+ <span class="cb-flow__sr" aria-live="polite">{{ announcement() }}</span>
1217
+ `, isInline: true, styles: [":host{--_start: var(--cb-color-success-600);--_decision: var(--cb-color-warning-600);--_end: var(--cb-color-text-strong);--_line: var(--cb-color-text-muted);display:flex;flex-direction:column;box-sizing:border-box;width:100%;overflow:hidden;border:var(--cb-border-width, 1px) solid var(--cb-color-border);border-radius:var(--cb-radius-lg);background:var(--cb-color-surface);color:var(--cb-color-text);font-family:var(--cb-font-sans);font-size:var(--cb-text-sm)}.cb-flow__bar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--cb-space-2) var(--cb-space-3);padding:var(--cb-space-2) var(--cb-space-3);border-bottom:var(--cb-border-width, 1px) solid var(--cb-color-border)}.cb-flow__group{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--cb-space-1)}.cb-flow__spacer{flex:1}.cb-flow__btn{display:inline-flex;align-items:center;gap:.375rem;height:2.25rem;padding:0 var(--cb-space-3);border:var(--cb-border-width, 1px) solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface);color:var(--cb-color-text);font:inherit;white-space:nowrap;cursor:pointer}.cb-flow__icon{display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;padding:0;border:0;border-radius:var(--cb-radius-md);background:transparent;color:var(--cb-color-text);cursor:pointer}.cb-flow__btn:hover:not(:disabled),.cb-flow__icon:hover:not(:disabled){background:var(--cb-color-surface-hover)}.cb-flow__btn[aria-pressed=true]{border-color:var(--cb-color-primary-500);background:color-mix(in srgb,var(--cb-color-primary-500) 14%,transparent);color:var(--cb-color-primary-700)}:host-context([data-theme=\"dark\"]) .cb-flow__btn[aria-pressed=true]{color:var(--cb-color-primary-300)}.cb-flow__btn:disabled,.cb-flow__icon:disabled{opacity:var(--cb-opacity-disabled, .5);cursor:default}.cb-flow__swatch{width:.75rem;height:.75rem;border:2px solid var(--cb-color-border-strong);border-radius:3px;background:var(--cb-color-surface-raised, var(--cb-color-surface))}.cb-flow__swatch--start,.cb-flow__swatch--end{border-radius:var(--cb-radius-full)}.cb-flow__swatch--start{border-color:var(--_start)}.cb-flow__swatch--end{border-color:var(--_end);border-width:3px}.cb-flow__swatch--decision{border-color:var(--_decision);transform:rotate(45deg) scale(.85)}.cb-flow__percent{min-width:3.25rem;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs);text-align:center;font-variant-numeric:tabular-nums}.cb-flow__views{display:inline-flex;gap:2px;padding:2px;border-radius:var(--cb-radius-md);background:var(--cb-color-surface-hover)}.cb-flow__view{padding:.3rem .75rem;border:0;border-radius:calc(var(--cb-radius-md) - 2px);background:transparent;color:var(--cb-color-text-muted);font:inherit;font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-medium);cursor:pointer}.cb-flow__view[aria-pressed=true]{background:var(--cb-color-surface-raised, var(--cb-color-surface));color:var(--cb-color-text-strong);box-shadow:0 1px 2px #0f172a1f}.cb-flow__btn:focus-visible,.cb-flow__icon:focus-visible,.cb-flow__view:focus-visible,.cb-flow__field:focus-visible,.cb-flow__issue-link:focus-visible,.cb-flow__node:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-flow__viewport{position:relative;height:var(--cb-flow-height, 32rem);overflow:hidden;background-color:var(--cb-color-surface-subtle);background-image:radial-gradient(circle,color-mix(in srgb,var(--cb-color-border-strong) 70%,transparent) 1px,transparent 1.5px);cursor:grab;touch-action:none;outline:none}.cb-flow__viewport--panning{cursor:grabbing}.cb-flow__viewport--connect,.cb-flow__viewport--connect .cb-flow__node{cursor:crosshair}.cb-flow__empty{position:absolute;inset:0;display:grid;place-items:center;margin:0;padding:var(--cb-space-6);color:var(--cb-color-text-muted);text-align:center;pointer-events:none}.cb-flow__world{position:absolute;top:0;left:0;transform-origin:0 0}.cb-flow__node{position:absolute;z-index:2;display:flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0 var(--cb-space-3);border-radius:var(--cb-radius-md);color:var(--cb-color-text-strong);cursor:move;-webkit-user-select:none;user-select:none;touch-action:none}.cb-flow__shape{position:absolute;inset:0;width:100%;height:100%;overflow:visible}.cb-flow__shape :is(rect,polygon){fill:var(--cb-color-surface-raised, var(--cb-color-surface));stroke:var(--cb-color-border-strong);stroke-width:1.5}.cb-flow__node--start .cb-flow__shape rect{fill:color-mix(in srgb,var(--_start) 14%,var(--cb-color-surface));stroke:var(--_start);stroke-width:2}.cb-flow__node--end .cb-flow__shape rect{fill:color-mix(in srgb,var(--_end) 8%,var(--cb-color-surface));stroke:var(--_end);stroke-width:3}.cb-flow__node--decision .cb-flow__shape polygon{fill:color-mix(in srgb,var(--_decision) 14%,var(--cb-color-surface));stroke:var(--_decision);stroke-width:2}.cb-flow__node--decision{padding:0 36px}.cb-flow__node.cb-is-selected .cb-flow__shape :is(rect,polygon){stroke:var(--cb-color-primary-500);stroke-width:3}.cb-flow__node.cb-is-source .cb-flow__shape :is(rect,polygon){stroke:var(--cb-color-primary-500);stroke-dasharray:6 4;stroke-width:3}.cb-flow__label{position:relative;min-width:0;max-width:100%;display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:.8125rem;font-weight:var(--cb-font-weight-medium);line-height:1.25;text-align:center;overflow-wrap:anywhere}.cb-flow__badge{position:absolute;top:-.5rem;right:-.25rem;display:grid;place-items:center;width:1.125rem;height:1.125rem;border:2px solid var(--cb-color-surface);border-radius:var(--cb-radius-full);background:var(--cb-color-danger-600);color:#fff;font-size:.6875rem;font-weight:var(--cb-font-weight-bold);line-height:1}.cb-flow__node--decision .cb-flow__badge{right:28px}.cb-flow__handle{position:absolute;left:50%;bottom:0;width:.875rem;height:.875rem;border:2px solid var(--cb-color-surface);border-radius:var(--cb-radius-full);background:var(--cb-color-primary-500);transform:translate(-50%,50%);cursor:crosshair;opacity:0;transition:opacity var(--cb-duration-fast, .12s) var(--cb-ease, ease)}.cb-flow__node:hover .cb-flow__handle,.cb-flow__node:focus-visible .cb-flow__handle,.cb-flow__node.cb-is-selected .cb-flow__handle{opacity:1}@media(hover:none){.cb-flow__handle{opacity:1;width:1.25rem;height:1.25rem}}.cb-flow__edges{position:absolute;z-index:1;overflow:visible;pointer-events:none}.cb-flow__edge{outline:none;cursor:pointer}.cb-flow__edge-hit{fill:none;stroke:transparent;stroke-width:14;pointer-events:stroke}.cb-flow__edge-line{fill:none;stroke:var(--_line);stroke-width:1.75}.cb-flow__edge-arrow{fill:var(--_line)}.cb-flow__edge:hover :is(.cb-flow__edge-line){stroke:var(--cb-color-text)}.cb-flow__edge:hover .cb-flow__edge-arrow{fill:var(--cb-color-text)}.cb-flow__edge.cb-is-selected .cb-flow__edge-line,.cb-flow__edge:focus-visible .cb-flow__edge-line{stroke:var(--cb-color-primary-500);stroke-width:3}.cb-flow__edge.cb-is-selected .cb-flow__edge-arrow,.cb-flow__edge:focus-visible .cb-flow__edge-arrow{fill:var(--cb-color-primary-500)}.cb-flow__edge:focus-visible .cb-flow__edge-hit{stroke:color-mix(in srgb,var(--cb-color-focus-ring) 35%,transparent);stroke-width:10}.cb-flow__edge-line--temp{stroke:var(--cb-color-primary-500);stroke-dasharray:6 4;stroke-width:2}.cb-flow__edge-label{position:absolute;z-index:3;padding:.125rem .5rem;border:var(--cb-border-width, 1px) solid var(--cb-color-border);border-radius:var(--cb-radius-full);background:var(--cb-color-surface);color:var(--cb-color-text);font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-medium);white-space:nowrap;transform:translate(-50%,-50%);cursor:pointer;-webkit-user-select:none;user-select:none}.cb-flow__edge-label.cb-is-selected{border-color:var(--cb-color-primary-500);color:var(--cb-color-primary-700)}:host-context([data-theme=\"dark\"]) .cb-flow__edge-label.cb-is-selected{color:var(--cb-color-primary-300)}.cb-flow__editor{position:absolute;z-index:5;box-sizing:border-box;height:30px;padding:0 var(--cb-space-2);border:2px solid var(--cb-color-primary-500);border-radius:var(--cb-radius-sm);background:var(--cb-color-surface);color:var(--cb-color-text);font:inherit;font-size:.8125rem;text-align:center;outline:none;box-shadow:var(--cb-shadow-md)}.cb-flow__list{display:flex;flex-direction:column;gap:var(--cb-space-2);max-height:var(--cb-flow-height, 32rem);margin:0;padding:var(--cb-space-3);overflow-y:auto;list-style:none;background:var(--cb-color-surface-subtle)}.cb-flow__empty--list{position:static;padding:var(--cb-space-4)}.cb-flow__item{display:flex;flex-direction:column;gap:var(--cb-space-2);padding:var(--cb-space-3);border:var(--cb-border-width, 1px) solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface)}.cb-flow__item--issue{border-left:3px solid var(--cb-color-danger-500)}.cb-flow__item-head{display:flex;flex-wrap:wrap;align-items:center;gap:var(--cb-space-2)}.cb-flow__item-name{flex:1;min-width:10rem}.cb-flow__type{flex:none;padding:.125rem .5rem;border-radius:var(--cb-radius-full);background:var(--cb-color-surface-hover);color:var(--cb-color-text);font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-semibold)}.cb-flow__type--start{background:color-mix(in srgb,var(--_start) 16%,transparent)}.cb-flow__type--decision{background:color-mix(in srgb,var(--_decision) 18%,transparent)}.cb-flow__type--end{background:color-mix(in srgb,var(--_end) 12%,transparent)}.cb-flow__field{box-sizing:border-box;height:2.25rem;padding:0 var(--cb-space-2);border:var(--cb-border-width, 1px) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-md);background:var(--cb-color-surface);color:var(--cb-color-text);font:inherit}.cb-flow__field--sm{height:2rem;width:9rem}.cb-flow__item-issue{margin:0;color:var(--cb-color-danger-700);font-size:var(--cb-text-xs)}:host-context([data-theme=\"dark\"]) .cb-flow__item-issue{color:var(--cb-color-danger-300)}.cb-flow__exits{display:flex;flex-direction:column;gap:var(--cb-space-1);margin:0;padding:0 0 0 var(--cb-space-4);list-style:none}.cb-flow__exit{display:flex;flex-wrap:wrap;align-items:center;gap:var(--cb-space-2)}.cb-flow__exit-target{font-weight:var(--cb-font-weight-medium)}.cb-flow__exit-label,.cb-flow__exit--none{color:var(--cb-color-text-muted)}.cb-flow__connect-row{display:flex;flex-wrap:wrap;gap:var(--cb-space-2);padding-left:var(--cb-space-4)}.cb-flow__issues,.cb-flow__valid{margin:0;padding:var(--cb-space-2) var(--cb-space-3);border-top:var(--cb-border-width, 1px) solid var(--cb-color-border)}.cb-flow__issues summary{color:var(--cb-color-danger-700);font-weight:var(--cb-font-weight-semibold);cursor:pointer}:host-context([data-theme=\"dark\"]) .cb-flow__issues summary{color:var(--cb-color-danger-300)}.cb-flow__issues-list{margin:var(--cb-space-2) 0 0;padding-left:var(--cb-space-5)}.cb-flow__issue--warning{color:var(--cb-color-text-muted)}.cb-flow__issue-link{padding:0;border:0;background:none;color:inherit;font:inherit;text-align:left;text-decoration:underline;text-underline-offset:2px;cursor:pointer}.cb-flow__valid{display:flex;align-items:center;gap:var(--cb-space-2);color:var(--cb-color-success-700)}:host-context([data-theme=\"dark\"]) .cb-flow__valid{color:var(--cb-color-success-300)}.cb-flow__sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}@media(forced-colors:active){.cb-flow__shape :is(rect,polygon){stroke:CanvasText}.cb-flow__node.cb-is-selected .cb-flow__shape :is(rect,polygon),.cb-flow__edge.cb-is-selected .cb-flow__edge-line{stroke:Highlight}.cb-flow__edge-line{stroke:CanvasText}.cb-flow__edge-arrow{fill:CanvasText}}\n"], dependencies: [{ kind: "component", type: CbIcon, selector: "cb-icon", inputs: ["name", "size", "spin", "ariaLabel"] }], changeDetection: i0.ChangeDetectionStrategy.OnPush }); }
1218
+ }
1219
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "22.0.5", ngImport: i0, type: CbFlowEditor, decorators: [{
1220
+ type: Component,
1221
+ args: [{ selector: 'cb-flow-editor', changeDetection: ChangeDetectionStrategy.OnPush, encapsulation: ViewEncapsulation.Emulated, imports: [CbIcon], host: {
1222
+ class: 'cb-flow',
1223
+ role: 'group',
1224
+ '[attr.aria-label]': 'ariaLabel() || t().flowEditor',
1225
+ '(keydown)': 'onHostKeydown($event)',
1226
+ }, template: `
1227
+ <div class="cb-flow__bar">
1228
+ @if (!readonly()) {
1229
+ <div class="cb-flow__group" role="group" [attr.aria-label]="t().flowAdd">
1230
+ @for (type of types; track type) {
1231
+ <button type="button" class="cb-flow__btn" (click)="addNode(type)">
1232
+ <span class="cb-flow__swatch" [class]="'cb-flow__swatch cb-flow__swatch--' + type" aria-hidden="true"></span>
1233
+ {{ typeLabel(type) }}
1234
+ </button>
1235
+ }
1236
+ </div>
1237
+ <div class="cb-flow__group">
1238
+ <button
1239
+ type="button"
1240
+ class="cb-flow__btn"
1241
+ [attr.aria-pressed]="!!connectFrom()"
1242
+ [disabled]="!selectedNode() || selectedNode()!.type === 'end'"
1243
+ (click)="toggleConnect()"
1244
+ >
1245
+ <cb-icon name="spline" [size]="16" aria-hidden="true" />{{ t().flowConnect }}
1246
+ </button>
1247
+ <button type="button" class="cb-flow__btn" [disabled]="!selected() || !selectedExists()" (click)="deleteSelected()">
1248
+ <cb-icon name="trash" [size]="16" aria-hidden="true" />{{ t().flowDelete }}
1249
+ </button>
1250
+ <button type="button" class="cb-flow__btn" [disabled]="!flow().nodes.length" (click)="layout()">
1251
+ <cb-icon name="workflow" [size]="16" aria-hidden="true" />{{ t().flowLayout }}
1252
+ </button>
1253
+ </div>
1254
+ }
1255
+ @if (view() === 'diagram') {
1256
+ <div class="cb-flow__group" role="group" [attr.aria-label]="t().flowZoom">
1257
+ <button type="button" class="cb-flow__icon" [attr.aria-label]="t().flowZoomOut" [disabled]="zoom() <= minZoom" (click)="zoomBy(1 / step)">
1258
+ <cb-icon name="zoom-out" [size]="18" aria-hidden="true" />
1259
+ </button>
1260
+ <span class="cb-flow__percent" aria-hidden="true">{{ percent() }}</span>
1261
+ <button type="button" class="cb-flow__icon" [attr.aria-label]="t().flowZoomIn" [disabled]="zoom() >= maxZoom" (click)="zoomBy(step)">
1262
+ <cb-icon name="zoom-in" [size]="18" aria-hidden="true" />
1263
+ </button>
1264
+ <button type="button" class="cb-flow__icon" [attr.aria-label]="t().flowFit" [attr.title]="t().flowFit" [disabled]="!flow().nodes.length" (click)="fit()">
1265
+ <cb-icon name="scan" [size]="18" aria-hidden="true" />
1266
+ </button>
1267
+ </div>
1268
+ }
1269
+ <span class="cb-flow__spacer"></span>
1270
+ <div class="cb-flow__views" role="group" [attr.aria-label]="t().flowView">
1271
+ <button type="button" class="cb-flow__view" [attr.aria-pressed]="view() === 'diagram'" (click)="view.set('diagram')">{{ t().flowDiagram }}</button>
1272
+ <button type="button" class="cb-flow__view" [attr.aria-pressed]="view() === 'list'" (click)="view.set('list')">{{ t().flowList }}</button>
1273
+ </div>
1274
+ </div>
1275
+
1276
+ @if (view() === 'diagram') {
1277
+ <div
1278
+ #viewport
1279
+ class="cb-flow__viewport"
1280
+ tabindex="-1"
1281
+ [class.cb-flow__viewport--connect]="!!connectFrom()"
1282
+ [class.cb-flow__viewport--panning]="panning()"
1283
+ [style.background-size]="gridBackground().size"
1284
+ [style.background-position]="gridBackground().position"
1285
+ (pointerdown)="onBackgroundPointerDown($event)"
1286
+ (wheel)="onWheel($event)"
1287
+ >
1288
+ @if (!flow().nodes.length) {
1289
+ <p class="cb-flow__empty">{{ t().flowEmpty }}</p>
1290
+ }
1291
+ <div class="cb-flow__world" [style.transform]="worldTransform()">
1292
+ @for (n of nodes(); track n.id) {
1293
+ <div
1294
+ class="cb-flow__node"
1295
+ [class]="'cb-flow__node cb-flow__node--' + n.type"
1296
+ [class.cb-is-selected]="selected() === n.id"
1297
+ [class.cb-is-source]="connectFrom() === n.id"
1298
+ [class.cb-flow__node--issue]="issuesByNode().has(n.id)"
1299
+ [attr.data-node]="n.id"
1300
+ tabindex="0"
1301
+ role="button"
1302
+ [attr.aria-label]="nodeLabel(n)"
1303
+ [attr.aria-describedby]="uid + (readonly() ? '-ro-hint' : '-node-hint')"
1304
+ [style.left.px]="n.x"
1305
+ [style.top.px]="n.y"
1306
+ [style.width.px]="sizes[n.type].w"
1307
+ [style.height.px]="sizes[n.type].h"
1308
+ (pointerdown)="onNodePointerDown($event, n)"
1309
+ (dblclick)="onNodeDblClick(n)"
1310
+ (keydown)="onNodeKeydown($event, n)"
1311
+ (focus)="selected.set(n.id)"
1312
+ >
1313
+ <svg class="cb-flow__shape" [attr.viewBox]="'0 0 ' + sizes[n.type].w + ' ' + sizes[n.type].h" aria-hidden="true">
1314
+ @switch (n.type) {
1315
+ @case ('decision') {
1316
+ <polygon [attr.points]="diamond(n.type)" />
1317
+ }
1318
+ @case ('task') {
1319
+ <rect x="1" y="1" rx="8" [attr.width]="sizes[n.type].w - 2" [attr.height]="sizes[n.type].h - 2" />
1320
+ }
1321
+ @default {
1322
+ <rect x="1" y="1" [attr.rx]="(sizes[n.type].h - 2) / 2" [attr.width]="sizes[n.type].w - 2" [attr.height]="sizes[n.type].h - 2" />
1323
+ }
1324
+ }
1325
+ </svg>
1326
+ <span class="cb-flow__label">{{ n.label }}</span>
1327
+ @if (issuesByNode().has(n.id)) {
1328
+ <span class="cb-flow__badge" aria-hidden="true">!</span>
1329
+ }
1330
+ @if (!readonly() && n.type !== 'end') {
1331
+ <span class="cb-flow__handle" aria-hidden="true" (pointerdown)="onHandlePointerDown($event, n)"></span>
1332
+ }
1333
+ </div>
1334
+ }
1335
+ <svg
1336
+ class="cb-flow__edges"
1337
+ [attr.viewBox]="svgBox().x + ' ' + svgBox().y + ' ' + svgBox().w + ' ' + svgBox().h"
1338
+ [style.left.px]="svgBox().x"
1339
+ [style.top.px]="svgBox().y"
1340
+ [style.width.px]="svgBox().w"
1341
+ [style.height.px]="svgBox().h"
1342
+ >
1343
+ @for (e of edgeViews(); track e.edge.id) {
1344
+ <g
1345
+ class="cb-flow__edge"
1346
+ [class.cb-is-selected]="selected() === e.edge.id"
1347
+ [attr.data-edge]="e.edge.id"
1348
+ tabindex="0"
1349
+ role="button"
1350
+ [attr.aria-label]="e.label"
1351
+ [attr.aria-describedby]="readonly() ? null : uid + '-edge-hint'"
1352
+ (pointerdown)="onEdgePointerDown($event, e.edge)"
1353
+ (dblclick)="startEdit(e.edge.id, 'edge')"
1354
+ (keydown)="onEdgeKeydown($event, e.edge)"
1355
+ (focus)="selected.set(e.edge.id)"
1356
+ >
1357
+ <path class="cb-flow__edge-hit" [attr.d]="e.d" />
1358
+ <path class="cb-flow__edge-line" [attr.d]="e.d" />
1359
+ <polygon class="cb-flow__edge-arrow" [attr.points]="e.arrow" />
1360
+ </g>
1361
+ }
1362
+ @if (tempLine(); as line) {
1363
+ <path class="cb-flow__edge-line cb-flow__edge-line--temp" [attr.d]="line" />
1364
+ }
1365
+ </svg>
1366
+ @for (e of edgeViews(); track e.edge.id) {
1367
+ @if (e.edge.label) {
1368
+ <span
1369
+ class="cb-flow__edge-label"
1370
+ [class.cb-is-selected]="selected() === e.edge.id"
1371
+ aria-hidden="true"
1372
+ [style.left.px]="e.mid[0]"
1373
+ [style.top.px]="e.mid[1]"
1374
+ (pointerdown)="onEdgePointerDown($event, e.edge)"
1375
+ (dblclick)="startEdit(e.edge.id, 'edge')"
1376
+ >{{ e.edge.label }}</span
1377
+ >
1378
+ }
1379
+ }
1380
+ @if (editorBox(); as box) {
1381
+ <input
1382
+ #editor
1383
+ class="cb-flow__editor"
1384
+ [attr.aria-label]="box.label"
1385
+ [style.left.px]="box.x"
1386
+ [style.top.px]="box.y"
1387
+ [style.width.px]="box.w"
1388
+ (keydown)="onEditorKeydown($event)"
1389
+ (blur)="commitEdit()"
1390
+ (pointerdown)="$event.stopPropagation()"
1391
+ />
1392
+ }
1393
+ </div>
1394
+ </div>
1395
+ } @else {
1396
+ <ol class="cb-flow__list" [attr.aria-label]="t().flowList">
1397
+ @if (!flow().nodes.length) {
1398
+ <li class="cb-flow__empty cb-flow__empty--list">{{ t().flowEmpty }}</li>
1399
+ }
1400
+ @for (n of ordered(); track n.id) {
1401
+ <li class="cb-flow__item" [class.cb-flow__item--issue]="issuesByNode().has(n.id)">
1402
+ <div class="cb-flow__item-head">
1403
+ <span class="cb-flow__type" [class]="'cb-flow__type cb-flow__type--' + n.type">{{ typeLabel(n.type) }}</span>
1404
+ @if (readonly()) {
1405
+ <strong class="cb-flow__item-name">{{ n.label }}</strong>
1406
+ } @else {
1407
+ <input
1408
+ class="cb-flow__field cb-flow__item-name"
1409
+ [attr.data-name]="n.id"
1410
+ [value]="n.label"
1411
+ [attr.aria-label]="format(t().flowNameOf, typeLabel(n.type))"
1412
+ (change)="rename(n.id, $any($event.target).value)"
1413
+ />
1414
+ <button type="button" class="cb-flow__icon" [attr.aria-label]="format(t().flowDeleteNode, n.label)" (click)="removeNode(n.id)">
1415
+ <cb-icon name="trash" [size]="16" aria-hidden="true" />
1416
+ </button>
1417
+ }
1418
+ </div>
1419
+ @for (issue of issuesByNode().get(n.id) ?? []; track $index) {
1420
+ <p class="cb-flow__item-issue">{{ issueText(issue) }}</p>
1421
+ }
1422
+ <ul class="cb-flow__exits" [attr.aria-label]="format(t().flowExitsOf, n.label)">
1423
+ @for (e of outgoing().get(n.id) ?? []; track e.id) {
1424
+ <li class="cb-flow__exit">
1425
+ <span aria-hidden="true">→</span>
1426
+ <span class="cb-flow__exit-target">{{ nameOf(e.to) }}</span>
1427
+ @if (readonly()) {
1428
+ @if (e.label) {
1429
+ <span class="cb-flow__exit-label">{{ e.label }}</span>
1430
+ }
1431
+ } @else {
1432
+ <input
1433
+ class="cb-flow__field cb-flow__field--sm"
1434
+ [value]="e.label ?? ''"
1435
+ [placeholder]="t().flowEdgeLabel"
1436
+ [attr.aria-label]="format(t().flowEdgeLabelOf, n.label, nameOf(e.to))"
1437
+ (change)="relabel(e.id, $any($event.target).value)"
1438
+ />
1439
+ <button type="button" class="cb-flow__icon" [attr.aria-label]="format(t().flowRemoveConnection, nameOf(e.to))" (click)="removeEdge(e.id)">
1440
+ <cb-icon name="x" [size]="16" aria-hidden="true" />
1441
+ </button>
1442
+ }
1443
+ </li>
1444
+ } @empty {
1445
+ <li class="cb-flow__exit cb-flow__exit--none">{{ t().flowNoExits }}</li>
1446
+ }
1447
+ </ul>
1448
+ @if (!readonly() && n.type !== 'end' && targetsFor(n).length) {
1449
+ <div class="cb-flow__connect-row">
1450
+ <select #target class="cb-flow__field" [attr.aria-label]="format(t().flowConnectTo, n.label)">
1451
+ <option value="">{{ t().flowChooseTarget }}</option>
1452
+ @for (o of targetsFor(n); track o.id) {
1453
+ <option [value]="o.id">{{ typeLabel(o.type) }}: {{ o.label }}</option>
1454
+ }
1455
+ </select>
1456
+ <button type="button" class="cb-flow__btn" (click)="connectFromList(n.id, target)">{{ t().flowAddConnection }}</button>
1457
+ </div>
1458
+ }
1459
+ </li>
1460
+ }
1461
+ </ol>
1462
+ }
1463
+
1464
+ @if (showValidation() && flow().nodes.length) {
1465
+ @if (issues().length) {
1466
+ <details class="cb-flow__issues" open>
1467
+ <summary>{{ format(t().flowIssues, issues().length) }}</summary>
1468
+ <ul class="cb-flow__issues-list">
1469
+ @for (issue of issues(); track $index) {
1470
+ <li [class.cb-flow__issue--warning]="issue.severity === 'warning'">
1471
+ @if (issue.nodeId) {
1472
+ <button type="button" class="cb-flow__issue-link" (click)="reveal(issue.nodeId)">{{ issueText(issue) }}</button>
1473
+ } @else {
1474
+ {{ issueText(issue) }}
1475
+ }
1476
+ </li>
1477
+ }
1478
+ </ul>
1479
+ </details>
1480
+ } @else {
1481
+ <p class="cb-flow__valid">
1482
+ <cb-icon name="check" [size]="16" aria-hidden="true" />{{ t().flowNoIssues }}
1483
+ </p>
1484
+ }
1485
+ }
1486
+
1487
+ <span class="cb-flow__sr" [id]="uid + '-node-hint'">{{ t().flowNodeHint }}</span>
1488
+ <span class="cb-flow__sr" [id]="uid + '-ro-hint'">{{ t().flowReadonlyHint }}</span>
1489
+ <span class="cb-flow__sr" [id]="uid + '-edge-hint'">{{ t().flowEdgeHint }}</span>
1490
+ <span class="cb-flow__sr" aria-live="polite">{{ announcement() }}</span>
1491
+ `, styles: [":host{--_start: var(--cb-color-success-600);--_decision: var(--cb-color-warning-600);--_end: var(--cb-color-text-strong);--_line: var(--cb-color-text-muted);display:flex;flex-direction:column;box-sizing:border-box;width:100%;overflow:hidden;border:var(--cb-border-width, 1px) solid var(--cb-color-border);border-radius:var(--cb-radius-lg);background:var(--cb-color-surface);color:var(--cb-color-text);font-family:var(--cb-font-sans);font-size:var(--cb-text-sm)}.cb-flow__bar{display:flex;flex-wrap:wrap;align-items:center;gap:var(--cb-space-2) var(--cb-space-3);padding:var(--cb-space-2) var(--cb-space-3);border-bottom:var(--cb-border-width, 1px) solid var(--cb-color-border)}.cb-flow__group{display:inline-flex;flex-wrap:wrap;align-items:center;gap:var(--cb-space-1)}.cb-flow__spacer{flex:1}.cb-flow__btn{display:inline-flex;align-items:center;gap:.375rem;height:2.25rem;padding:0 var(--cb-space-3);border:var(--cb-border-width, 1px) solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface);color:var(--cb-color-text);font:inherit;white-space:nowrap;cursor:pointer}.cb-flow__icon{display:inline-flex;align-items:center;justify-content:center;width:2.25rem;height:2.25rem;padding:0;border:0;border-radius:var(--cb-radius-md);background:transparent;color:var(--cb-color-text);cursor:pointer}.cb-flow__btn:hover:not(:disabled),.cb-flow__icon:hover:not(:disabled){background:var(--cb-color-surface-hover)}.cb-flow__btn[aria-pressed=true]{border-color:var(--cb-color-primary-500);background:color-mix(in srgb,var(--cb-color-primary-500) 14%,transparent);color:var(--cb-color-primary-700)}:host-context([data-theme=\"dark\"]) .cb-flow__btn[aria-pressed=true]{color:var(--cb-color-primary-300)}.cb-flow__btn:disabled,.cb-flow__icon:disabled{opacity:var(--cb-opacity-disabled, .5);cursor:default}.cb-flow__swatch{width:.75rem;height:.75rem;border:2px solid var(--cb-color-border-strong);border-radius:3px;background:var(--cb-color-surface-raised, var(--cb-color-surface))}.cb-flow__swatch--start,.cb-flow__swatch--end{border-radius:var(--cb-radius-full)}.cb-flow__swatch--start{border-color:var(--_start)}.cb-flow__swatch--end{border-color:var(--_end);border-width:3px}.cb-flow__swatch--decision{border-color:var(--_decision);transform:rotate(45deg) scale(.85)}.cb-flow__percent{min-width:3.25rem;color:var(--cb-color-text-muted);font-size:var(--cb-text-xs);text-align:center;font-variant-numeric:tabular-nums}.cb-flow__views{display:inline-flex;gap:2px;padding:2px;border-radius:var(--cb-radius-md);background:var(--cb-color-surface-hover)}.cb-flow__view{padding:.3rem .75rem;border:0;border-radius:calc(var(--cb-radius-md) - 2px);background:transparent;color:var(--cb-color-text-muted);font:inherit;font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-medium);cursor:pointer}.cb-flow__view[aria-pressed=true]{background:var(--cb-color-surface-raised, var(--cb-color-surface));color:var(--cb-color-text-strong);box-shadow:0 1px 2px #0f172a1f}.cb-flow__btn:focus-visible,.cb-flow__icon:focus-visible,.cb-flow__view:focus-visible,.cb-flow__field:focus-visible,.cb-flow__issue-link:focus-visible,.cb-flow__node:focus-visible{outline:2px solid var(--cb-color-focus-ring);outline-offset:2px}.cb-flow__viewport{position:relative;height:var(--cb-flow-height, 32rem);overflow:hidden;background-color:var(--cb-color-surface-subtle);background-image:radial-gradient(circle,color-mix(in srgb,var(--cb-color-border-strong) 70%,transparent) 1px,transparent 1.5px);cursor:grab;touch-action:none;outline:none}.cb-flow__viewport--panning{cursor:grabbing}.cb-flow__viewport--connect,.cb-flow__viewport--connect .cb-flow__node{cursor:crosshair}.cb-flow__empty{position:absolute;inset:0;display:grid;place-items:center;margin:0;padding:var(--cb-space-6);color:var(--cb-color-text-muted);text-align:center;pointer-events:none}.cb-flow__world{position:absolute;top:0;left:0;transform-origin:0 0}.cb-flow__node{position:absolute;z-index:2;display:flex;align-items:center;justify-content:center;box-sizing:border-box;padding:0 var(--cb-space-3);border-radius:var(--cb-radius-md);color:var(--cb-color-text-strong);cursor:move;-webkit-user-select:none;user-select:none;touch-action:none}.cb-flow__shape{position:absolute;inset:0;width:100%;height:100%;overflow:visible}.cb-flow__shape :is(rect,polygon){fill:var(--cb-color-surface-raised, var(--cb-color-surface));stroke:var(--cb-color-border-strong);stroke-width:1.5}.cb-flow__node--start .cb-flow__shape rect{fill:color-mix(in srgb,var(--_start) 14%,var(--cb-color-surface));stroke:var(--_start);stroke-width:2}.cb-flow__node--end .cb-flow__shape rect{fill:color-mix(in srgb,var(--_end) 8%,var(--cb-color-surface));stroke:var(--_end);stroke-width:3}.cb-flow__node--decision .cb-flow__shape polygon{fill:color-mix(in srgb,var(--_decision) 14%,var(--cb-color-surface));stroke:var(--_decision);stroke-width:2}.cb-flow__node--decision{padding:0 36px}.cb-flow__node.cb-is-selected .cb-flow__shape :is(rect,polygon){stroke:var(--cb-color-primary-500);stroke-width:3}.cb-flow__node.cb-is-source .cb-flow__shape :is(rect,polygon){stroke:var(--cb-color-primary-500);stroke-dasharray:6 4;stroke-width:3}.cb-flow__label{position:relative;min-width:0;max-width:100%;display:-webkit-box;overflow:hidden;-webkit-box-orient:vertical;-webkit-line-clamp:2;font-size:.8125rem;font-weight:var(--cb-font-weight-medium);line-height:1.25;text-align:center;overflow-wrap:anywhere}.cb-flow__badge{position:absolute;top:-.5rem;right:-.25rem;display:grid;place-items:center;width:1.125rem;height:1.125rem;border:2px solid var(--cb-color-surface);border-radius:var(--cb-radius-full);background:var(--cb-color-danger-600);color:#fff;font-size:.6875rem;font-weight:var(--cb-font-weight-bold);line-height:1}.cb-flow__node--decision .cb-flow__badge{right:28px}.cb-flow__handle{position:absolute;left:50%;bottom:0;width:.875rem;height:.875rem;border:2px solid var(--cb-color-surface);border-radius:var(--cb-radius-full);background:var(--cb-color-primary-500);transform:translate(-50%,50%);cursor:crosshair;opacity:0;transition:opacity var(--cb-duration-fast, .12s) var(--cb-ease, ease)}.cb-flow__node:hover .cb-flow__handle,.cb-flow__node:focus-visible .cb-flow__handle,.cb-flow__node.cb-is-selected .cb-flow__handle{opacity:1}@media(hover:none){.cb-flow__handle{opacity:1;width:1.25rem;height:1.25rem}}.cb-flow__edges{position:absolute;z-index:1;overflow:visible;pointer-events:none}.cb-flow__edge{outline:none;cursor:pointer}.cb-flow__edge-hit{fill:none;stroke:transparent;stroke-width:14;pointer-events:stroke}.cb-flow__edge-line{fill:none;stroke:var(--_line);stroke-width:1.75}.cb-flow__edge-arrow{fill:var(--_line)}.cb-flow__edge:hover :is(.cb-flow__edge-line){stroke:var(--cb-color-text)}.cb-flow__edge:hover .cb-flow__edge-arrow{fill:var(--cb-color-text)}.cb-flow__edge.cb-is-selected .cb-flow__edge-line,.cb-flow__edge:focus-visible .cb-flow__edge-line{stroke:var(--cb-color-primary-500);stroke-width:3}.cb-flow__edge.cb-is-selected .cb-flow__edge-arrow,.cb-flow__edge:focus-visible .cb-flow__edge-arrow{fill:var(--cb-color-primary-500)}.cb-flow__edge:focus-visible .cb-flow__edge-hit{stroke:color-mix(in srgb,var(--cb-color-focus-ring) 35%,transparent);stroke-width:10}.cb-flow__edge-line--temp{stroke:var(--cb-color-primary-500);stroke-dasharray:6 4;stroke-width:2}.cb-flow__edge-label{position:absolute;z-index:3;padding:.125rem .5rem;border:var(--cb-border-width, 1px) solid var(--cb-color-border);border-radius:var(--cb-radius-full);background:var(--cb-color-surface);color:var(--cb-color-text);font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-medium);white-space:nowrap;transform:translate(-50%,-50%);cursor:pointer;-webkit-user-select:none;user-select:none}.cb-flow__edge-label.cb-is-selected{border-color:var(--cb-color-primary-500);color:var(--cb-color-primary-700)}:host-context([data-theme=\"dark\"]) .cb-flow__edge-label.cb-is-selected{color:var(--cb-color-primary-300)}.cb-flow__editor{position:absolute;z-index:5;box-sizing:border-box;height:30px;padding:0 var(--cb-space-2);border:2px solid var(--cb-color-primary-500);border-radius:var(--cb-radius-sm);background:var(--cb-color-surface);color:var(--cb-color-text);font:inherit;font-size:.8125rem;text-align:center;outline:none;box-shadow:var(--cb-shadow-md)}.cb-flow__list{display:flex;flex-direction:column;gap:var(--cb-space-2);max-height:var(--cb-flow-height, 32rem);margin:0;padding:var(--cb-space-3);overflow-y:auto;list-style:none;background:var(--cb-color-surface-subtle)}.cb-flow__empty--list{position:static;padding:var(--cb-space-4)}.cb-flow__item{display:flex;flex-direction:column;gap:var(--cb-space-2);padding:var(--cb-space-3);border:var(--cb-border-width, 1px) solid var(--cb-color-border);border-radius:var(--cb-radius-md);background:var(--cb-color-surface)}.cb-flow__item--issue{border-left:3px solid var(--cb-color-danger-500)}.cb-flow__item-head{display:flex;flex-wrap:wrap;align-items:center;gap:var(--cb-space-2)}.cb-flow__item-name{flex:1;min-width:10rem}.cb-flow__type{flex:none;padding:.125rem .5rem;border-radius:var(--cb-radius-full);background:var(--cb-color-surface-hover);color:var(--cb-color-text);font-size:var(--cb-text-xs);font-weight:var(--cb-font-weight-semibold)}.cb-flow__type--start{background:color-mix(in srgb,var(--_start) 16%,transparent)}.cb-flow__type--decision{background:color-mix(in srgb,var(--_decision) 18%,transparent)}.cb-flow__type--end{background:color-mix(in srgb,var(--_end) 12%,transparent)}.cb-flow__field{box-sizing:border-box;height:2.25rem;padding:0 var(--cb-space-2);border:var(--cb-border-width, 1px) solid var(--cb-color-border-strong);border-radius:var(--cb-radius-md);background:var(--cb-color-surface);color:var(--cb-color-text);font:inherit}.cb-flow__field--sm{height:2rem;width:9rem}.cb-flow__item-issue{margin:0;color:var(--cb-color-danger-700);font-size:var(--cb-text-xs)}:host-context([data-theme=\"dark\"]) .cb-flow__item-issue{color:var(--cb-color-danger-300)}.cb-flow__exits{display:flex;flex-direction:column;gap:var(--cb-space-1);margin:0;padding:0 0 0 var(--cb-space-4);list-style:none}.cb-flow__exit{display:flex;flex-wrap:wrap;align-items:center;gap:var(--cb-space-2)}.cb-flow__exit-target{font-weight:var(--cb-font-weight-medium)}.cb-flow__exit-label,.cb-flow__exit--none{color:var(--cb-color-text-muted)}.cb-flow__connect-row{display:flex;flex-wrap:wrap;gap:var(--cb-space-2);padding-left:var(--cb-space-4)}.cb-flow__issues,.cb-flow__valid{margin:0;padding:var(--cb-space-2) var(--cb-space-3);border-top:var(--cb-border-width, 1px) solid var(--cb-color-border)}.cb-flow__issues summary{color:var(--cb-color-danger-700);font-weight:var(--cb-font-weight-semibold);cursor:pointer}:host-context([data-theme=\"dark\"]) .cb-flow__issues summary{color:var(--cb-color-danger-300)}.cb-flow__issues-list{margin:var(--cb-space-2) 0 0;padding-left:var(--cb-space-5)}.cb-flow__issue--warning{color:var(--cb-color-text-muted)}.cb-flow__issue-link{padding:0;border:0;background:none;color:inherit;font:inherit;text-align:left;text-decoration:underline;text-underline-offset:2px;cursor:pointer}.cb-flow__valid{display:flex;align-items:center;gap:var(--cb-space-2);color:var(--cb-color-success-700)}:host-context([data-theme=\"dark\"]) .cb-flow__valid{color:var(--cb-color-success-300)}.cb-flow__sr{position:absolute;width:1px;height:1px;margin:-1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap;border:0}@media(forced-colors:active){.cb-flow__shape :is(rect,polygon){stroke:CanvasText}.cb-flow__node.cb-is-selected .cb-flow__shape :is(rect,polygon),.cb-flow__edge.cb-is-selected .cb-flow__edge-line{stroke:Highlight}.cb-flow__edge-line{stroke:CanvasText}.cb-flow__edge-arrow{fill:CanvasText}}\n"] }]
1492
+ }], ctorParameters: () => [], propDecorators: { flow: [{ type: i0.Input, args: [{ isSignal: true, alias: "flow", required: false }] }, { type: i0.Output, args: ["flowChange"] }], readonly: [{ type: i0.Input, args: [{ isSignal: true, alias: "readonly", required: false }] }], gridSize: [{ type: i0.Input, args: [{ isSignal: true, alias: "gridSize", required: false }] }], zoom: [{ type: i0.Input, args: [{ isSignal: true, alias: "zoom", required: false }] }, { type: i0.Output, args: ["zoomChange"] }], view: [{ type: i0.Input, args: [{ isSignal: true, alias: "view", required: false }] }, { type: i0.Output, args: ["viewChange"] }], selected: [{ type: i0.Input, args: [{ isSignal: true, alias: "selected", required: false }] }, { type: i0.Output, args: ["selectedChange"] }], showValidation: [{ type: i0.Input, args: [{ isSignal: true, alias: "showValidation", required: false }] }], fitOnLoad: [{ type: i0.Input, args: [{ isSignal: true, alias: "fitOnLoad", required: false }] }], ariaLabel: [{ type: i0.Input, args: [{ isSignal: true, alias: "ariaLabel", required: false }] }], nodeOpen: [{ type: i0.Output, args: ["nodeOpen"] }], viewport: [{ type: i0.ViewChild, args: ['viewport', { isSignal: true }] }], editor: [{ type: i0.ViewChild, args: ['editor', { isSignal: true }] }] } });
1493
+ function clamp(value, min, max) {
1494
+ return Math.min(max, Math.max(min, value));
1495
+ }
1496
+ function round2(value) {
1497
+ return Math.round(value * 100) / 100;
1498
+ }
1499
+
1500
+ /*
1501
+ * Public API Surface de @caribeng/ui/flow
1502
+ */
1503
+
1504
+ /**
1505
+ * Generated bundle index. Do not edit.
1506
+ */
1507
+
1508
+ export { CB_FLOW_NODE_SIZE, CbFlowEditor, cbLayoutFlow, cbValidateFlow };