@revenexx/studio-integrations 0.1.4 → 0.3.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 (188) hide show
  1. package/README.md +31 -0
  2. package/dist/module.d.mts +2 -2
  3. package/dist/module.json +1 -1
  4. package/dist/runtime/components/Integrations/CredentialCreateDialog.d.vue.ts +13 -0
  5. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue +279 -0
  6. package/dist/runtime/components/Integrations/CredentialCreateDialog.vue.d.ts +13 -0
  7. package/dist/runtime/components/Integrations/CredentialDetailDialog.d.vue.ts +20 -0
  8. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue +342 -0
  9. package/dist/runtime/components/Integrations/CredentialDetailDialog.vue.d.ts +20 -0
  10. package/dist/runtime/components/Integrations/CredentialLogo.d.vue.ts +14 -0
  11. package/dist/runtime/components/Integrations/CredentialLogo.vue +37 -0
  12. package/dist/runtime/components/Integrations/CredentialLogo.vue.d.ts +14 -0
  13. package/dist/runtime/components/Integrations/CredentialRevenexxSource.d.vue.ts +12 -0
  14. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue +364 -0
  15. package/dist/runtime/components/Integrations/CredentialRevenexxSource.vue.d.ts +12 -0
  16. package/dist/runtime/components/Integrations/CredentialTypePicker.d.vue.ts +15 -0
  17. package/dist/runtime/components/Integrations/CredentialTypePicker.vue +71 -0
  18. package/dist/runtime/components/Integrations/CredentialTypePicker.vue.d.ts +15 -0
  19. package/dist/runtime/components/Integrations/DeadLettersPanel.d.vue.ts +5 -0
  20. package/dist/runtime/components/Integrations/DeadLettersPanel.vue +151 -0
  21. package/dist/runtime/components/Integrations/DeadLettersPanel.vue.d.ts +5 -0
  22. package/dist/runtime/components/Integrations/EditorTip.d.vue.ts +17 -0
  23. package/dist/runtime/components/Integrations/EditorTip.vue +46 -0
  24. package/dist/runtime/components/Integrations/EditorTip.vue.d.ts +17 -0
  25. package/dist/runtime/components/Integrations/EventTypeCombobox.d.vue.ts +9 -0
  26. package/dist/runtime/components/Integrations/EventTypeCombobox.vue +64 -0
  27. package/dist/runtime/components/Integrations/EventTypeCombobox.vue.d.ts +9 -0
  28. package/dist/runtime/components/Integrations/ExpressionInput.d.vue.ts +19 -0
  29. package/dist/runtime/components/Integrations/ExpressionInput.vue +236 -0
  30. package/dist/runtime/components/Integrations/ExpressionInput.vue.d.ts +19 -0
  31. package/dist/runtime/components/Integrations/GroupFrameView.d.vue.ts +28 -0
  32. package/dist/runtime/components/Integrations/GroupFrameView.vue +222 -0
  33. package/dist/runtime/components/Integrations/GroupFrameView.vue.d.ts +28 -0
  34. package/dist/runtime/components/Integrations/NodeConfigDynamicField.d.vue.ts +14 -0
  35. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue +184 -0
  36. package/dist/runtime/components/Integrations/NodeConfigDynamicField.vue.d.ts +14 -0
  37. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.d.vue.ts +16 -0
  38. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue +102 -0
  39. package/dist/runtime/components/Integrations/NodeConfigDynamicSchema.vue.d.ts +16 -0
  40. package/dist/runtime/components/Integrations/NodeConfigField.d.vue.ts +23 -0
  41. package/dist/runtime/components/Integrations/NodeConfigField.vue +587 -0
  42. package/dist/runtime/components/Integrations/NodeConfigField.vue.d.ts +23 -0
  43. package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.d.vue.ts +14 -0
  44. package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.vue +80 -0
  45. package/dist/runtime/components/Integrations/NodeDynamicOutputsResolver.vue.d.ts +14 -0
  46. package/dist/runtime/components/Integrations/NodeInspector.d.vue.ts +22 -3
  47. package/dist/runtime/components/Integrations/NodeInspector.vue +1074 -294
  48. package/dist/runtime/components/Integrations/NodeInspector.vue.d.ts +22 -3
  49. package/dist/runtime/components/Integrations/NodeLogo.d.vue.ts +9 -0
  50. package/dist/runtime/components/Integrations/NodeLogo.vue +38 -0
  51. package/dist/runtime/components/Integrations/NodeLogo.vue.d.ts +9 -0
  52. package/dist/runtime/components/Integrations/NodePalette.d.vue.ts +22 -1
  53. package/dist/runtime/components/Integrations/NodePalette.vue +411 -90
  54. package/dist/runtime/components/Integrations/NodePalette.vue.d.ts +22 -1
  55. package/dist/runtime/components/Integrations/QueryBuilder.d.vue.ts +9 -0
  56. package/dist/runtime/components/Integrations/QueryBuilder.vue +239 -0
  57. package/dist/runtime/components/Integrations/QueryBuilder.vue.d.ts +9 -0
  58. package/dist/runtime/components/Integrations/{RunDetailDialog.d.vue.ts → RunActionsMenu.d.vue.ts} +3 -4
  59. package/dist/runtime/components/Integrations/RunActionsMenu.vue +86 -0
  60. package/dist/runtime/components/Integrations/{RunDetailDialog.vue.d.ts → RunActionsMenu.vue.d.ts} +3 -4
  61. package/dist/runtime/components/Integrations/RunDetailPanel.d.vue.ts +14 -0
  62. package/dist/runtime/components/Integrations/RunDetailPanel.vue +777 -0
  63. package/dist/runtime/components/Integrations/RunDetailPanel.vue.d.ts +14 -0
  64. package/dist/runtime/components/Integrations/RunPayloadInput.d.vue.ts +13 -0
  65. package/dist/runtime/components/Integrations/RunPayloadInput.vue +175 -0
  66. package/dist/runtime/components/Integrations/RunPayloadInput.vue.d.ts +13 -0
  67. package/dist/runtime/components/Integrations/RunResponseModal.d.vue.ts +27 -0
  68. package/dist/runtime/components/Integrations/RunResponseModal.vue +84 -0
  69. package/dist/runtime/components/Integrations/RunResponseModal.vue.d.ts +27 -0
  70. package/dist/runtime/components/Integrations/RunsTable.d.vue.ts +21 -0
  71. package/dist/runtime/components/Integrations/RunsTable.vue +114 -0
  72. package/dist/runtime/components/Integrations/RunsTable.vue.d.ts +21 -0
  73. package/dist/runtime/components/Integrations/SftpAccountImportDialog.d.vue.ts +11 -0
  74. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue +91 -0
  75. package/dist/runtime/components/Integrations/SftpAccountImportDialog.vue.d.ts +11 -0
  76. package/dist/runtime/components/Integrations/StickyNoteView.d.vue.ts +28 -0
  77. package/dist/runtime/components/Integrations/StickyNoteView.vue +208 -0
  78. package/dist/runtime/components/Integrations/StickyNoteView.vue.d.ts +28 -0
  79. package/dist/runtime/components/Integrations/TemplateFlowPreview.d.vue.ts +29 -0
  80. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue +201 -0
  81. package/dist/runtime/components/Integrations/TemplateFlowPreview.vue.d.ts +29 -0
  82. package/dist/runtime/components/Integrations/WorkflowContextMenu.d.vue.ts +32 -0
  83. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue +138 -0
  84. package/dist/runtime/components/Integrations/WorkflowContextMenu.vue.d.ts +32 -0
  85. package/dist/runtime/components/Integrations/WorkflowCreateDialog.d.vue.ts +9 -0
  86. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue +322 -0
  87. package/dist/runtime/components/Integrations/WorkflowCreateDialog.vue.d.ts +9 -0
  88. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.d.vue.ts +18 -0
  89. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue +63 -0
  90. package/dist/runtime/components/Integrations/WorkflowDuplicateDialog.vue.d.ts +18 -0
  91. package/dist/runtime/components/Integrations/WorkflowEdgeView.d.vue.ts +12 -1
  92. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue +68 -6
  93. package/dist/runtime/components/Integrations/WorkflowEdgeView.vue.d.ts +12 -1
  94. package/dist/runtime/components/Integrations/WorkflowEditor.d.vue.ts +8 -1
  95. package/dist/runtime/components/Integrations/WorkflowEditor.vue +1424 -144
  96. package/dist/runtime/components/Integrations/WorkflowEditor.vue.d.ts +8 -1
  97. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.d.vue.ts +12 -0
  98. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue +417 -0
  99. package/dist/runtime/components/Integrations/WorkflowHistoryPanel.vue.d.ts +12 -0
  100. package/dist/runtime/components/Integrations/WorkflowIconPicker.d.vue.ts +10 -0
  101. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue +89 -0
  102. package/dist/runtime/components/Integrations/WorkflowIconPicker.vue.d.ts +10 -0
  103. package/dist/runtime/components/Integrations/WorkflowIconView.d.vue.ts +10 -0
  104. package/dist/runtime/components/Integrations/WorkflowIconView.vue +35 -0
  105. package/dist/runtime/components/Integrations/WorkflowIconView.vue.d.ts +10 -0
  106. package/dist/runtime/components/Integrations/WorkflowNodeView.d.vue.ts +5 -1
  107. package/dist/runtime/components/Integrations/WorkflowNodeView.vue +114 -84
  108. package/dist/runtime/components/Integrations/WorkflowNodeView.vue.d.ts +5 -1
  109. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.d.vue.ts +12 -0
  110. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue +654 -0
  111. package/dist/runtime/components/Integrations/WorkflowRunReplayPanel.vue.d.ts +12 -0
  112. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.d.vue.ts +36 -0
  113. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue +136 -0
  114. package/dist/runtime/components/Integrations/WorkflowSettingsDialog.vue.d.ts +36 -0
  115. package/dist/runtime/components/Integrations/WorkflowTagsInput.d.vue.ts +14 -0
  116. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue +63 -0
  117. package/dist/runtime/components/Integrations/WorkflowTagsInput.vue.d.ts +14 -0
  118. package/dist/runtime/components/Integrations/WorkflowTriggerView.vue +35 -15
  119. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.d.vue.ts +13 -0
  120. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue +265 -0
  121. package/dist/runtime/components/Integrations/WorkflowVariablesDialog.vue.d.ts +13 -0
  122. package/dist/runtime/components/Integrations/WorkflowsTable.d.vue.ts +4 -1
  123. package/dist/runtime/components/Integrations/WorkflowsTable.vue +122 -39
  124. package/dist/runtime/components/Integrations/WorkflowsTable.vue.d.ts +4 -1
  125. package/dist/runtime/components/Integrations/workflowLayout.d.ts +5 -0
  126. package/dist/runtime/components/Integrations/workflowLayout.js +5 -3
  127. package/dist/runtime/composables/integrationsApi.d.ts +6 -7
  128. package/dist/runtime/composables/integrationsApi.js +6 -12
  129. package/dist/runtime/composables/useCanvasShortcuts.d.ts +37 -0
  130. package/dist/runtime/composables/useCanvasShortcuts.js +62 -0
  131. package/dist/runtime/composables/useCredentialTypes.js +2 -1
  132. package/dist/runtime/composables/useIntegrationsAudit.js +2 -1
  133. package/dist/runtime/composables/useIntegrationsCredentials.js +2 -1
  134. package/dist/runtime/composables/useIntegrationsDeadLetters.d.ts +22 -0
  135. package/dist/runtime/composables/useIntegrationsDeadLetters.js +50 -0
  136. package/dist/runtime/composables/useIntegrationsNodes.d.ts +4 -0
  137. package/dist/runtime/composables/useIntegrationsNodes.js +8 -4
  138. package/dist/runtime/composables/useIntegrationsRevisions.d.ts +40 -0
  139. package/dist/runtime/composables/useIntegrationsRevisions.js +52 -0
  140. package/dist/runtime/composables/useIntegrationsRuns.d.ts +21 -1
  141. package/dist/runtime/composables/useIntegrationsRuns.js +94 -5
  142. package/dist/runtime/composables/useIntegrationsSecrets.js +2 -1
  143. package/dist/runtime/composables/useIntegrationsTemplates.d.ts +1 -0
  144. package/dist/runtime/composables/useIntegrationsTemplates.js +34 -5
  145. package/dist/runtime/composables/useIntegrationsTriggers.js +2 -1
  146. package/dist/runtime/composables/useIntegrationsWorkflows.d.ts +1 -0
  147. package/dist/runtime/composables/useIntegrationsWorkflows.js +59 -2
  148. package/dist/runtime/composables/useMiniMapLayout.d.ts +34 -0
  149. package/dist/runtime/composables/useMiniMapLayout.js +119 -0
  150. package/dist/runtime/composables/useNodeConfigResolve.d.ts +43 -0
  151. package/dist/runtime/composables/useNodeConfigResolve.js +124 -0
  152. package/dist/runtime/composables/useNodeDrag.d.ts +22 -0
  153. package/dist/runtime/composables/useNodeDrag.js +10 -0
  154. package/dist/runtime/composables/usePaletteLayout.d.ts +41 -0
  155. package/dist/runtime/composables/usePaletteLayout.js +113 -0
  156. package/dist/runtime/data/expressionEditor.d.ts +22 -0
  157. package/dist/runtime/data/expressionEditor.js +40 -0
  158. package/dist/runtime/data/integrationNodeCatalog.js +8 -8
  159. package/dist/runtime/data/nodeInputData.d.ts +37 -0
  160. package/dist/runtime/data/nodeInputData.js +62 -0
  161. package/dist/runtime/data/nodeRunData.d.ts +56 -0
  162. package/dist/runtime/data/nodeRunData.js +91 -0
  163. package/dist/runtime/data/payloadForm.d.ts +44 -0
  164. package/dist/runtime/data/payloadForm.js +126 -0
  165. package/dist/runtime/data/workflowIconCatalog.d.ts +34 -0
  166. package/dist/runtime/data/workflowIconCatalog.js +116 -0
  167. package/dist/runtime/pages/integrations/audit.vue +25 -35
  168. package/dist/runtime/pages/integrations/credentials.vue +117 -122
  169. package/dist/runtime/pages/integrations/index.vue +133 -36
  170. package/dist/runtime/pages/integrations/runs.vue +125 -75
  171. package/dist/runtime/pages/integrations/schedules.vue +5 -16
  172. package/dist/runtime/pages/integrations/secrets.vue +1 -3
  173. package/dist/runtime/pages/integrations/workflows/[id].vue +599 -194
  174. package/dist/runtime/pages/integrations/workflows/index.vue +10 -8
  175. package/dist/runtime/types/integrations.d.ts +190 -4
  176. package/dist/runtime/utils/apiError.d.ts +17 -0
  177. package/dist/runtime/utils/apiError.js +8 -0
  178. package/dist/runtime/utils/credentialState.d.ts +28 -0
  179. package/dist/runtime/utils/credentialState.js +29 -0
  180. package/dist/runtime/utils/localize.d.ts +3 -1
  181. package/dist/runtime/utils/localize.js +1 -2
  182. package/dist/runtime/utils/statusStyle.d.ts +6 -0
  183. package/dist/runtime/utils/statusStyle.js +15 -0
  184. package/package.json +20 -3
  185. package/dist/runtime/components/Integrations/RunDetailDialog.vue +0 -169
  186. package/dist/runtime/pages/integrations/workflows/new.d.vue.ts +0 -2
  187. package/dist/runtime/pages/integrations/workflows/new.vue +0 -153
  188. package/dist/runtime/pages/integrations/workflows/new.vue.d.ts +0 -2
@@ -0,0 +1,208 @@
1
+ <script setup>
2
+ import { computed, nextTick, ref, watch } from "vue";
3
+ import { Position, useVueFlow } from "@vue-flow/core";
4
+ import { NodeToolbar } from "@vue-flow/node-toolbar";
5
+ import { Trash2 } from "lucide-vue-next";
6
+ const props = defineProps({
7
+ id: { type: String, required: true },
8
+ data: { type: Object, required: true },
9
+ selected: { type: Boolean, required: false }
10
+ });
11
+ defineOptions({ inheritAttrs: false });
12
+ defineEmits(["updateNodeInternals"]);
13
+ const flow = useVueFlow();
14
+ const STICKY_COLORS = [
15
+ { bg: "#fef3c7", border: "#fde68a", name: "Amber" },
16
+ { bg: "#ffedd5", border: "#fed7aa", name: "Orange" },
17
+ { bg: "#ffe4e6", border: "#fecdd3", name: "Rose" },
18
+ { bg: "#dcfce7", border: "#bbf7d0", name: "Green" },
19
+ { bg: "#dbeafe", border: "#bfdbfe", name: "Blue" },
20
+ { bg: "#ede9fe", border: "#ddd6fe", name: "Violet" },
21
+ { bg: "#f5f5f4", border: "#e7e5e4", name: "Gray" }
22
+ ];
23
+ const color = computed(() => STICKY_COLORS[props.data.sticky.color ?? 0] ?? STICKY_COLORS[0]);
24
+ function pickColor(i) {
25
+ if ((props.data.sticky.color ?? 0) === i) return;
26
+ props.data.onUpdate({ color: i });
27
+ }
28
+ const DEFAULT_WIDTH = 240;
29
+ const DEFAULT_HEIGHT = 160;
30
+ const MIN_WIDTH = 150;
31
+ const MIN_HEIGHT = 80;
32
+ const width = ref(props.data.sticky.size?.width ?? DEFAULT_WIDTH);
33
+ const height = ref(props.data.sticky.size?.height ?? DEFAULT_HEIGHT);
34
+ const resizing = ref(false);
35
+ watch(() => props.data.sticky.size, (s) => {
36
+ if (resizing.value) return;
37
+ width.value = s?.width ?? DEFAULT_WIDTH;
38
+ height.value = s?.height ?? DEFAULT_HEIGHT;
39
+ });
40
+ let resizeStart = { x: 0, y: 0, w: 0, h: 0 };
41
+ function onResizeStart(e) {
42
+ e.stopPropagation();
43
+ e.preventDefault();
44
+ resizing.value = true;
45
+ resizeStart = { x: e.clientX, y: e.clientY, w: width.value, h: height.value };
46
+ e.target.setPointerCapture(e.pointerId);
47
+ }
48
+ function onResizeMove(e) {
49
+ if (!resizing.value) return;
50
+ const zoom = flow.viewport.value.zoom || 1;
51
+ width.value = Math.max(MIN_WIDTH, Math.round(resizeStart.w + (e.clientX - resizeStart.x) / zoom));
52
+ height.value = Math.max(MIN_HEIGHT, Math.round(resizeStart.h + (e.clientY - resizeStart.y) / zoom));
53
+ }
54
+ function onResizeEnd() {
55
+ if (!resizing.value) return;
56
+ resizing.value = false;
57
+ props.data.onUpdate({ size: { width: width.value, height: height.value } });
58
+ }
59
+ const editing = ref(false);
60
+ const draft = ref("");
61
+ const textareaEl = ref(null);
62
+ function startEdit() {
63
+ if (editing.value) return;
64
+ draft.value = props.data.sticky.content;
65
+ editing.value = true;
66
+ nextTick(() => textareaEl.value?.focus());
67
+ }
68
+ function commitEdit() {
69
+ if (!editing.value) return;
70
+ editing.value = false;
71
+ if (draft.value !== props.data.sticky.content) {
72
+ props.data.onUpdate({ content: draft.value });
73
+ }
74
+ }
75
+ function onTextareaKeydown(e) {
76
+ e.stopPropagation();
77
+ if (e.key === "Escape") commitEdit();
78
+ }
79
+ watch(editing, (v) => {
80
+ flow.updateNode(props.id, { zIndex: v ? 10 : -100 });
81
+ });
82
+ const hovered = ref(false);
83
+ let hoverTimer = null;
84
+ function onHoverEnter() {
85
+ if (hoverTimer) clearTimeout(hoverTimer);
86
+ hovered.value = true;
87
+ }
88
+ function onHoverLeave() {
89
+ if (hoverTimer) clearTimeout(hoverTimer);
90
+ hoverTimer = setTimeout(() => {
91
+ hovered.value = false;
92
+ }, 250);
93
+ }
94
+ const toolbarVisible = computed(() => !editing.value && (hovered.value || !!props.selected));
95
+ function escapeHtml(s) {
96
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
97
+ }
98
+ function inline(s) {
99
+ return s.replace(/`([^`\n]+)`/g, '<code class="rounded bg-black/[0.07] px-1 py-px font-mono text-[0.85em]">$1</code>').replace(/\[([^\]]+)\]\((https?:\/\/[^\s)]+)\)/g, '<a href="$2" target="_blank" rel="noopener noreferrer" class="font-medium underline decoration-current/40 underline-offset-2 hover:decoration-current">$1</a>').replace(/\*\*([^*]+)\*\*/g, "<strong>$1</strong>").replace(/(^|[^*])\*([^*\n]+)\*(?!\*)/g, "$1<em>$2</em>");
100
+ }
101
+ function renderMarkdownLite(src) {
102
+ return escapeHtml(src).split("\n").map((line) => {
103
+ const h = /^(#{1,3})\s+(.+)$/.exec(line);
104
+ if (h) {
105
+ const cls = h[1].length === 1 ? "text-base font-semibold" : h[1].length === 2 ? "text-sm font-semibold" : "text-sm font-medium";
106
+ return `<div class="${cls}">${inline(h[2])}</div>`;
107
+ }
108
+ const ul = /^[-*]\s+(.+)$/.exec(line);
109
+ if (ul) return `<div class="flex gap-1.5 pl-1"><span class="select-none">\u2022</span><span class="min-w-0">${inline(ul[1])}</span></div>`;
110
+ const ol = /^(\d+)\.\s+(.+)$/.exec(line);
111
+ if (ol) return `<div class="flex gap-1.5 pl-1"><span class="select-none tabular-nums">${ol[1]}.</span><span class="min-w-0">${inline(ol[2])}</span></div>`;
112
+ return line ? `<div>${inline(line)}</div>` : "<div>&nbsp;</div>";
113
+ }).join("");
114
+ }
115
+ const rendered = computed(() => renderMarkdownLite(props.data.sticky.content));
116
+ function onViewMouseDown(e) {
117
+ if (e.target?.closest("a")) e.stopPropagation();
118
+ }
119
+ </script>
120
+
121
+ <template>
122
+ <!-- Hover/selection toolbar: color palette + delete -->
123
+ <NodeToolbar :is-visible="toolbarVisible" :position="Position.Top" :offset="8">
124
+ <div
125
+ class="flex items-center gap-1 rounded-lg border border-black/[0.06] bg-white px-1.5 py-1 shadow-md"
126
+ @mouseenter="onHoverEnter"
127
+ @mouseleave="onHoverLeave"
128
+ >
129
+ <button
130
+ v-for="(c, i) in STICKY_COLORS"
131
+ :key="i"
132
+ type="button"
133
+ class="size-5 rounded-full border transition hover:scale-110"
134
+ :class="(data.sticky.color ?? 0) === i ? 'ring-2 ring-black/25 ring-offset-1' : ''"
135
+ :style="{ backgroundColor: c.bg, borderColor: c.border }"
136
+ :title="c.name"
137
+ @click="pickColor(i)"
138
+ />
139
+ <div class="mx-0.5 h-4 w-px bg-black/10" />
140
+ <button
141
+ type="button"
142
+ class="flex size-6 items-center justify-center rounded-md text-muted-foreground transition hover:bg-rose-50 hover:text-rose-600"
143
+ title="Delete note"
144
+ @click="data.onRemove()"
145
+ >
146
+ <Trash2 :size="13" />
147
+ </button>
148
+ </div>
149
+ </NodeToolbar>
150
+
151
+ <!-- Card -->
152
+ <div
153
+ class="relative flex flex-col overflow-hidden rounded-lg border text-sm leading-snug text-stone-700 shadow-sm"
154
+ :class="[
155
+ selected ? 'ring-2 ring-emerald-400/60' : '',
156
+ editing ? '' : 'cursor-grab active:cursor-grabbing'
157
+ ]"
158
+ :style="{
159
+ width: `${width}px`,
160
+ height: `${height}px`,
161
+ backgroundColor: color.bg,
162
+ borderColor: color.border
163
+ }"
164
+ @mouseenter="onHoverEnter"
165
+ @mouseleave="onHoverLeave"
166
+ @dblclick.stop="startEdit"
167
+ >
168
+ <!-- View mode -->
169
+ <div
170
+ v-if="!editing"
171
+ class="h-full w-full overflow-hidden whitespace-pre-wrap break-words px-3 py-2"
172
+ @mousedown="onViewMouseDown"
173
+ >
174
+ <div v-if="data.sticky.content" v-html="rendered" />
175
+ <span v-else class="select-none text-stone-400">Double-click to edit</span>
176
+ </div>
177
+
178
+ <!-- Edit mode — nodrag/nowheel so typing and scrolling never drag or zoom
179
+ the canvas; keydown is stopped so Delete/Shift+S stay in the textarea. -->
180
+ <textarea
181
+ v-else
182
+ ref="textareaEl"
183
+ v-model="draft"
184
+ class="nodrag nowheel h-full w-full resize-none bg-transparent px-3 py-2 text-sm leading-snug text-stone-700 outline-none placeholder:text-stone-400"
185
+ placeholder="Write a note… (# heading, **bold**, *italic*, `code`, [link](https://…), - list)"
186
+ spellcheck="false"
187
+ @blur="commitEdit"
188
+ @keydown="onTextareaKeydown"
189
+ @mousedown.stop
190
+ @dblclick.stop
191
+ />
192
+
193
+ <!-- Bottom-right resize handle (hand-rolled; min 150x80) -->
194
+ <div
195
+ class="nodrag absolute bottom-0 right-0 flex size-4 cursor-se-resize items-end justify-end p-0.5 text-stone-400/80"
196
+ title="Resize"
197
+ @pointerdown="onResizeStart"
198
+ @pointermove="onResizeMove"
199
+ @pointerup="onResizeEnd"
200
+ @pointercancel="onResizeEnd"
201
+ >
202
+ <svg viewBox="0 0 8 8" class="size-2.5" fill="none" stroke="currentColor" stroke-width="1.2" stroke-linecap="round">
203
+ <path d="M7 1 1 7" />
204
+ <path d="M7 4.5 4.5 7" />
205
+ </svg>
206
+ </div>
207
+ </div>
208
+ </template>
@@ -0,0 +1,28 @@
1
+ import type { WorkflowSticky } from '../../types/integrations.js';
2
+ /**
3
+ * Sticky note — custom vue-flow node type `sticky`.
4
+ *
5
+ * An engine-irrelevant annotation card persisted in `blob.stickies`. The
6
+ * editor (WorkflowEditor.vue) renders stickies with a negative zIndex so they
7
+ * sit BEHIND workflow/trigger nodes, and injects the persistence callbacks
8
+ * through `data` — this component never touches the blob itself.
9
+ *
10
+ * - Double-click → textarea edit; click-out (blur) or Esc commits.
11
+ * - NodeToolbar on hover/selection: 7-color pastel palette + delete.
12
+ * - Hand-rolled bottom-right resize handle (no @vue-flow/node-resizer dep);
13
+ * size persists once on pointer-up.
14
+ */
15
+ export interface StickyNodeData {
16
+ sticky: WorkflowSticky;
17
+ /** Persist a partial change into the blob (editor-side, syncs + pushes). */
18
+ onUpdate: (patch: Partial<Pick<WorkflowSticky, 'content' | 'size' | 'color'>>) => void;
19
+ /** Remove this sticky from the blob (editor-side). */
20
+ onRemove: () => void;
21
+ }
22
+ type __VLS_Props = {
23
+ id: string;
24
+ data: StickyNodeData;
25
+ selected?: boolean;
26
+ };
27
+ declare const _default: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, any, string, import("vue").PublicProps, any, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
28
+ export default _default;
@@ -0,0 +1,29 @@
1
+ import type { WorkflowBlob } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ blob: WorkflowBlob | null;
4
+ /** Host box height in px; width follows the container. */
5
+ height?: number;
6
+ /** Extra logical padding around the graph. */
7
+ padding?: number;
8
+ /**
9
+ * Show error wiring (Stop and Error nodes + rose edges). Off by default:
10
+ * the gallery sells the happy path; failure plumbing is editor detail.
11
+ */
12
+ showErrorBranches?: boolean;
13
+ };
14
+ declare var __VLS_12: {};
15
+ type __VLS_Slots = {} & {
16
+ empty?: (props: typeof __VLS_12) => any;
17
+ };
18
+ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
19
+ height: number;
20
+ padding: number;
21
+ showErrorBranches: boolean;
22
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
23
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
24
+ export default _default;
25
+ type __VLS_WithSlots<T, S> = T & {
26
+ new (): {
27
+ $slots: S;
28
+ };
29
+ };
@@ -0,0 +1,201 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import * as Lucide from "lucide-vue-next";
4
+ import { findManifest, NODE_CATEGORIES, TRIGGER_TEMPLATES } from "../../data/integrationNodeCatalog";
5
+ import { resolveLocalized } from "../../utils/localize";
6
+ import IntegrationsNodeLogo from "./NodeLogo.vue";
7
+ const props = defineProps({
8
+ blob: { type: [Object, null], required: true },
9
+ height: { type: Number, required: false, default: 144 },
10
+ padding: { type: Number, required: false, default: 36 },
11
+ showErrorBranches: { type: Boolean, required: false, default: false }
12
+ });
13
+ const STOP_SLUG = "revenexx:stop-and-error";
14
+ const NODE_W = 220;
15
+ const NODE_H = 58;
16
+ const TRIG_W = 200;
17
+ const TRIG_H = 84;
18
+ const boxes = computed(() => {
19
+ const blob = props.blob;
20
+ if (!blob) return [];
21
+ const out = [];
22
+ const triggers = blob.triggers ?? [];
23
+ triggers.forEach((t, i) => {
24
+ const pos = blob.triggerPositions?.[t.id] ?? { x: 40, y: 40 + i * 160 };
25
+ const template = TRIGGER_TEMPLATES.find((tt) => tt.type === t.type);
26
+ const subtitle = t.type === "schedule" && t.config?.cron ? String(t.config.cron) : t.type === "webhook" && t.config?.path ? String(t.config.path) : resolveLocalized(template?.description ?? "");
27
+ out.push({
28
+ key: `t-${t.id}`,
29
+ kind: "trigger",
30
+ x: pos.x,
31
+ y: pos.y,
32
+ w: TRIG_W,
33
+ h: TRIG_H,
34
+ label: typeof t.name === "string" && t.name || t.type.charAt(0).toUpperCase() + t.type.slice(1),
35
+ subtitle,
36
+ type: t.type,
37
+ manifest: null,
38
+ icon: iconFor(template?.icon ?? "Zap")
39
+ });
40
+ });
41
+ blob.nodes.forEach((n, i) => {
42
+ if (!props.showErrorBranches && n.nodeSlug === STOP_SLUG) return;
43
+ const pos = n.position ?? { x: 320 + i * 260, y: 60 };
44
+ const manifest = findManifest(n.nodeSlug, n.nodeVersion);
45
+ const groups = (manifest?.groups ?? []).map((g) => resolveLocalized(g)).filter(Boolean);
46
+ out.push({
47
+ key: `n-${n.id}`,
48
+ kind: "node",
49
+ x: pos.x,
50
+ y: pos.y,
51
+ w: NODE_W,
52
+ h: NODE_H,
53
+ label: manifest ? resolveLocalized(manifest.name) : prettify(n.nodeSlug),
54
+ subtitle: groups[groups.length - 1] ?? "",
55
+ manifest,
56
+ icon: iconFor(manifest?.icon),
57
+ categoryColor: NODE_CATEGORIES.find((c) => c.key === manifest?.category)?.color ?? "bg-muted text-foreground"
58
+ });
59
+ });
60
+ return out;
61
+ });
62
+ function hasLogo(m) {
63
+ return (m?.images ?? []).some((i) => i.category === "logo");
64
+ }
65
+ function prettify(slug) {
66
+ const tail = slug.includes(":") ? slug.split(":").pop() : slug;
67
+ return tail.split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join(" ");
68
+ }
69
+ function iconFor(name) {
70
+ if (!name) return Lucide.Box;
71
+ const cleaned = (name.includes(":") ? name.split(":").pop() : name).split("-").map((s) => s.charAt(0).toUpperCase() + s.slice(1)).join("");
72
+ return Lucide[cleaned] || Lucide.Box;
73
+ }
74
+ function stepPath(x1, y1, x2, y2) {
75
+ if (y1 === y2) return `M ${x1} ${y1} L ${x2} ${y2}`;
76
+ if (x2 - x1 >= 48) {
77
+ const midX = x1 + (x2 - x1) / 2;
78
+ return `M ${x1} ${y1} L ${midX} ${y1} L ${midX} ${y2} L ${x2} ${y2}`;
79
+ }
80
+ const outX = x1 + 24;
81
+ return `M ${x1} ${y1} L ${outX} ${y1} L ${outX} ${y2} L ${x2} ${y2}`;
82
+ }
83
+ const edgePaths = computed(() => {
84
+ const blob = props.blob;
85
+ if (!blob) return [];
86
+ const byKey = /* @__PURE__ */ new Map();
87
+ for (const b of boxes.value) byKey.set(b.key.slice(2), b);
88
+ return (blob.edges ?? []).flatMap((e) => {
89
+ if (!props.showErrorBranches && e.from.output === "error") return [];
90
+ const from = byKey.get(e.from.nodeId);
91
+ const to = byKey.get(e.to.nodeId);
92
+ if (!from || !to) return [];
93
+ const x1 = from.x + from.w;
94
+ const y1 = from.y + from.h / 2;
95
+ const x2 = to.x;
96
+ const y2 = to.y + to.h / 2;
97
+ const tone = e.from.output === "error" ? "error" : from.kind === "trigger" ? "trigger" : "default";
98
+ return [{ key: e.id, d: stepPath(x1, y1, x2, y2), tone }];
99
+ });
100
+ });
101
+ const EDGE_STROKE = {
102
+ default: "#94a3b8",
103
+ trigger: "#10b981",
104
+ error: "#f43f5e"
105
+ };
106
+ const bounds = computed(() => {
107
+ if (!boxes.value.length) return { x: 0, y: 0, w: 1, h: 1 };
108
+ const minX = Math.min(...boxes.value.map((b) => b.x)) - props.padding;
109
+ const minY = Math.min(...boxes.value.map((b) => b.y)) - props.padding;
110
+ const maxX = Math.max(...boxes.value.map((b) => b.x + b.w)) + props.padding;
111
+ const maxY = Math.max(...boxes.value.map((b) => b.y + b.h)) + props.padding;
112
+ return { x: minX, y: minY, w: maxX - minX, h: maxY - minY };
113
+ });
114
+ </script>
115
+
116
+ <template>
117
+ <div
118
+ class="relative w-full overflow-hidden"
119
+ :style="{ height: `${height}px` }"
120
+ >
121
+ <svg
122
+ v-if="boxes.length"
123
+ class="absolute inset-0 h-full w-full"
124
+ :viewBox="`${bounds.x} ${bounds.y} ${bounds.w} ${bounds.h}`"
125
+ preserveAspectRatio="xMidYMid meet"
126
+ >
127
+ <defs>
128
+ <marker
129
+ v-for="(stroke, tone) in EDGE_STROKE"
130
+ :id="`tpl-arrow-${tone}`"
131
+ :key="tone"
132
+ viewBox="0 0 10 10"
133
+ refX="9"
134
+ refY="5"
135
+ markerWidth="7"
136
+ markerHeight="7"
137
+ orient="auto-start-reverse"
138
+ >
139
+ <path d="M 0 0 L 10 5 L 0 10 z" :fill="stroke" />
140
+ </marker>
141
+ </defs>
142
+ <path
143
+ v-for="e in edgePaths"
144
+ :key="e.key"
145
+ :d="e.d"
146
+ fill="none"
147
+ :stroke="EDGE_STROKE[e.tone]"
148
+ :stroke-width="1.75"
149
+ :marker-end="`url(#tpl-arrow-${e.tone})`"
150
+ />
151
+ <foreignObject
152
+ v-for="b in boxes"
153
+ :key="b.key"
154
+ :x="b.x"
155
+ :y="b.y"
156
+ :width="b.w"
157
+ :height="b.h"
158
+ >
159
+ <!-- Trigger — mirrors WorkflowTriggerView (gradient card + footer strip). -->
160
+ <div
161
+ v-if="b.kind === 'trigger'"
162
+ class="flex h-full w-full flex-col rounded-lg border-2 border-emerald-400/40 bg-gradient-to-br from-emerald-50 to-white shadow-sm"
163
+ >
164
+ <div class="flex flex-1 items-center gap-2.5 px-3 py-2">
165
+ <span class="flex size-8 shrink-0 items-center justify-center rounded-md bg-emerald-500 text-white shadow-sm">
166
+ <component :is="b.icon" :size="15" :stroke-width="2.5" />
167
+ </span>
168
+ <div class="min-w-0 flex-1">
169
+ <div class="truncate text-sm font-semibold leading-tight text-foreground">{{ b.label }}</div>
170
+ <div v-if="b.subtitle" class="mt-0.5 truncate text-sm text-muted-foreground">{{ b.subtitle }}</div>
171
+ </div>
172
+ </div>
173
+ <div class="rounded-b-md border-t border-emerald-500/15 bg-emerald-500/[0.04] px-3 py-1 text-mono font-semibold text-emerald-700/80">
174
+ Trigger · {{ b.type }}
175
+ </div>
176
+ </div>
177
+
178
+ <!-- Node — mirrors WorkflowNodeView (white card, size-8 icon chip). -->
179
+ <div
180
+ v-else
181
+ class="flex h-full w-full items-center gap-2.5 rounded-lg border border-black/[0.08] bg-white px-3 py-2.5 shadow-sm"
182
+ >
183
+ <span
184
+ class="flex size-8 shrink-0 items-center justify-center rounded-md"
185
+ :class="hasLogo(b.manifest) ? 'bg-white ring-1 ring-black/[0.06]' : b.categoryColor"
186
+ >
187
+ <IntegrationsNodeLogo v-if="hasLogo(b.manifest)" :manifest="b.manifest" :size="18" />
188
+ <component :is="b.icon" v-else :size="15" :stroke-width="2.25" />
189
+ </span>
190
+ <div class="min-w-0 flex-1">
191
+ <div class="truncate text-sm font-semibold leading-tight text-foreground">{{ b.label }}</div>
192
+ <div v-if="b.subtitle" class="mt-0.5 truncate text-sm text-muted-foreground">{{ b.subtitle }}</div>
193
+ </div>
194
+ </div>
195
+ </foreignObject>
196
+ </svg>
197
+ <div v-else class="flex h-full items-center justify-center text-sm text-muted-foreground">
198
+ <slot name="empty">Preview loading…</slot>
199
+ </div>
200
+ </div>
201
+ </template>
@@ -0,0 +1,29 @@
1
+ import type { WorkflowBlob } from '../../types/integrations.js';
2
+ type __VLS_Props = {
3
+ blob: WorkflowBlob | null;
4
+ /** Host box height in px; width follows the container. */
5
+ height?: number;
6
+ /** Extra logical padding around the graph. */
7
+ padding?: number;
8
+ /**
9
+ * Show error wiring (Stop and Error nodes + rose edges). Off by default:
10
+ * the gallery sells the happy path; failure plumbing is editor detail.
11
+ */
12
+ showErrorBranches?: boolean;
13
+ };
14
+ declare var __VLS_12: {};
15
+ type __VLS_Slots = {} & {
16
+ empty?: (props: typeof __VLS_12) => any;
17
+ };
18
+ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {}, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{}>, {
19
+ height: number;
20
+ padding: number;
21
+ showErrorBranches: boolean;
22
+ }, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
23
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
24
+ export default _default;
25
+ type __VLS_WithSlots<T, S> = T & {
26
+ new (): {
27
+ $slots: S;
28
+ };
29
+ };
@@ -0,0 +1,32 @@
1
+ export type MenuKind = 'node' | 'selection' | 'canvas';
2
+ export type MenuActionId = 'open' | 'rename' | 'copy' | 'duplicate' | 'paste' | 'run' | 'tidy-up' | 'fit-view' | 'add-node' | 'add-sticky' | 'group' | 'select-all' | 'clear-selection' | 'delete';
3
+ type __VLS_Props = {
4
+ /** Which context was right-clicked. The editor sets this in the vue-flow hooks. */
5
+ kind: MenuKind;
6
+ /** Number of currently selected nodes (drives selection labels/guards). */
7
+ selectionCount: number;
8
+ /** Total nodes on the canvas (disables "Select all" when everything is selected). */
9
+ totalNodes: number;
10
+ /** True when the single targeted node is a trigger (duplicate disabled in v1). */
11
+ targetIsTrigger?: boolean;
12
+ /** Busy flag for the Run item. */
13
+ runBusy?: boolean;
14
+ };
15
+ declare var __VLS_14: {};
16
+ type __VLS_Slots = {} & {
17
+ default?: (props: typeof __VLS_14) => any;
18
+ };
19
+ declare const __VLS_component: import("vue").DefineComponent<__VLS_Props, {}, {}, {}, {}, import("vue").ComponentOptionsMixin, import("vue").ComponentOptionsMixin, {
20
+ action: (id: MenuActionId) => any;
21
+ "update:open": (open: boolean) => any;
22
+ }, string, import("vue").PublicProps, Readonly<__VLS_Props> & Readonly<{
23
+ onAction?: ((id: MenuActionId) => any) | undefined;
24
+ "onUpdate:open"?: ((open: boolean) => any) | undefined;
25
+ }>, {}, {}, {}, {}, string, import("vue").ComponentProvideOptions, false, {}, any>;
26
+ declare const _default: __VLS_WithSlots<typeof __VLS_component, __VLS_Slots>;
27
+ export default _default;
28
+ type __VLS_WithSlots<T, S> = T & {
29
+ new (): {
30
+ $slots: S;
31
+ };
32
+ };
@@ -0,0 +1,138 @@
1
+ <script setup>
2
+ import { computed } from "vue";
3
+ import {
4
+ ContextMenu,
5
+ ContextMenuContent,
6
+ ContextMenuItem,
7
+ ContextMenuLabel,
8
+ ContextMenuSeparator,
9
+ ContextMenuShortcut,
10
+ ContextMenuTrigger
11
+ } from "@revenexx/studio";
12
+ const props = defineProps({
13
+ kind: { type: String, required: true },
14
+ selectionCount: { type: Number, required: true },
15
+ totalNodes: { type: Number, required: true },
16
+ targetIsTrigger: { type: Boolean, required: false },
17
+ runBusy: { type: Boolean, required: false }
18
+ });
19
+ const emit = defineEmits(["action", "update:open"]);
20
+ const isMac = typeof navigator !== "undefined" && /Mac|iPhone|iPad/.test(navigator.platform ?? "");
21
+ function formatShortcut(tokens) {
22
+ const parts = tokens.split("+");
23
+ const out = [];
24
+ for (const raw of parts) {
25
+ const t = raw.toLowerCase();
26
+ if (t === "mod") out.push(isMac ? "\u2318" : "Ctrl");
27
+ else if (t === "shift") out.push("\u21E7");
28
+ else if (t === "alt") out.push(isMac ? "\u2325" : "Alt");
29
+ else if (t === "enter") out.push("\u21B5");
30
+ else if (t === "backspace") out.push("\u232B");
31
+ else if (t === "delete") out.push("Del");
32
+ else if (t === "escape") out.push("Esc");
33
+ else if (t === "tab") out.push("Tab");
34
+ else if (t === "space") out.push("Space");
35
+ else if (t === "f2") out.push("F2");
36
+ else out.push(t.toUpperCase());
37
+ }
38
+ return isMac ? out.join("") : out.join("+");
39
+ }
40
+ const allSelected = computed(() => props.totalNodes > 0 && props.selectionCount >= props.totalNodes);
41
+ const sections = computed(() => {
42
+ if (props.kind === "node") {
43
+ return [
44
+ [
45
+ { id: "open", label: "Open", shortcut: "enter" },
46
+ { id: "rename", label: "Rename", shortcut: "f2" }
47
+ ],
48
+ [
49
+ { id: "copy", label: "Copy", shortcut: "mod+c", disabled: props.targetIsTrigger },
50
+ { id: "duplicate", label: "Duplicate", shortcut: "mod+d", disabled: props.targetIsTrigger }
51
+ ],
52
+ [
53
+ { id: "run", label: "Run workflow", shortcut: "mod+enter", disabled: props.runBusy },
54
+ { id: "tidy-up", label: "Tidy up", shortcut: "shift+alt+t" }
55
+ ],
56
+ [
57
+ { id: "delete", label: "Delete", shortcut: "backspace", danger: true }
58
+ ]
59
+ ];
60
+ }
61
+ if (props.kind === "selection") {
62
+ const n = props.selectionCount;
63
+ return [
64
+ [
65
+ { id: "copy", label: `Copy ${n} nodes`, shortcut: "mod+c" },
66
+ { id: "duplicate", label: `Duplicate ${n} nodes`, shortcut: "mod+d" }
67
+ ],
68
+ [
69
+ { id: "group", label: `Group ${n} nodes`, shortcut: "mod+g" },
70
+ { id: "tidy-up", label: "Tidy up", shortcut: "shift+alt+t" },
71
+ { id: "select-all", label: "Select all", shortcut: "mod+a", disabled: allSelected.value },
72
+ { id: "clear-selection", label: "Clear selection", shortcut: "escape" }
73
+ ],
74
+ [
75
+ { id: "delete", label: `Delete ${n} nodes`, shortcut: "backspace", danger: true }
76
+ ]
77
+ ];
78
+ }
79
+ return [
80
+ [
81
+ { id: "add-node", label: "Add node\u2026", shortcut: "tab" },
82
+ { id: "add-sticky", label: "Add sticky", shortcut: "shift+s" },
83
+ { id: "paste", label: "Paste", shortcut: "mod+v" }
84
+ ],
85
+ [
86
+ { id: "tidy-up", label: "Tidy up", shortcut: "shift+alt+t" },
87
+ { id: "fit-view", label: "Fit view", shortcut: "1" }
88
+ ],
89
+ [
90
+ { id: "select-all", label: "Select all", shortcut: "mod+a", disabled: allSelected.value || props.totalNodes === 0 }
91
+ ]
92
+ ];
93
+ });
94
+ const headerLabel = computed(
95
+ () => props.kind === "selection" ? `${props.selectionCount} nodes selected` : null
96
+ );
97
+ function onSelect(item) {
98
+ if (item.disabled) return;
99
+ emit("action", item.id);
100
+ }
101
+ </script>
102
+
103
+ <template>
104
+ <ContextMenu @update:open="(v) => emit('update:open', v)">
105
+ <!-- The canvas. as-child avoids an extra wrapper element so vue-flow's
106
+ 100%-height layout is untouched. -->
107
+ <ContextMenuTrigger as-child>
108
+ <slot />
109
+ </ContextMenuTrigger>
110
+
111
+ <ContextMenuContent class="w-60 rounded-lg border border-black/[0.06] bg-white p-1 shadow-lg">
112
+ <template v-if="headerLabel">
113
+ <ContextMenuLabel class="px-2 py-1.5 text-sm font-medium text-muted-foreground">
114
+ {{ headerLabel }}
115
+ </ContextMenuLabel>
116
+ <ContextMenuSeparator class="my-1 h-px bg-black/[0.06]" />
117
+ </template>
118
+
119
+ <template v-for="(group, gi) in sections" :key="gi">
120
+ <ContextMenuSeparator v-if="gi > 0" class="my-1 h-px bg-black/[0.06]" />
121
+ <ContextMenuItem
122
+ v-for="item in group"
123
+ :key="item.id"
124
+ :disabled="item.disabled"
125
+ :variant="item.danger ? 'destructive' : 'default'"
126
+ class="rounded-md px-2 py-1.5 text-sm"
127
+ @select="onSelect(item)"
128
+ >
129
+ <span class="flex-1 truncate">{{ item.label }}</span>
130
+ <span v-if="item.hint" class="ml-2 text-sm text-muted-foreground">{{ item.hint }}</span>
131
+ <ContextMenuShortcut v-if="item.shortcut" class="ml-auto pl-4 text-sm tabular-nums">
132
+ {{ formatShortcut(item.shortcut) }}
133
+ </ContextMenuShortcut>
134
+ </ContextMenuItem>
135
+ </template>
136
+ </ContextMenuContent>
137
+ </ContextMenu>
138
+ </template>