@andrewyang/ai-workflow-editor 0.1.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 (65) hide show
  1. package/.nuxt/app.config.mjs +18 -0
  2. package/.nuxt/components.d.ts +910 -0
  3. package/.nuxt/dev/index.mjs +4103 -0
  4. package/.nuxt/dev/index.mjs.map +1 -0
  5. package/.nuxt/dist/server/client.manifest.mjs +4 -0
  6. package/.nuxt/dist/server/client.precomputed.mjs +1 -0
  7. package/.nuxt/dist/server/server.mjs +1 -0
  8. package/.nuxt/i18n-route-resources.mjs +3 -0
  9. package/.nuxt/imports.d.ts +43 -0
  10. package/.nuxt/manifest/latest.json +1 -0
  11. package/.nuxt/manifest/meta/dev.json +1 -0
  12. package/.nuxt/nitro.json +17 -0
  13. package/.nuxt/nuxt.d.ts +24 -0
  14. package/.nuxt/nuxt.json +9 -0
  15. package/.nuxt/schema/nuxt.schema.d.ts +17 -0
  16. package/.nuxt/schema/nuxt.schema.json +3 -0
  17. package/.nuxt/tsconfig.json +234 -0
  18. package/.nuxt/tsconfig.server.json +185 -0
  19. package/.nuxt/types/app-defaults.d.ts +7 -0
  20. package/.nuxt/types/app.config.d.ts +31 -0
  21. package/.nuxt/types/build.d.ts +29 -0
  22. package/.nuxt/types/builder-env.d.ts +1 -0
  23. package/.nuxt/types/components.d.ts +915 -0
  24. package/.nuxt/types/i18n-plugin.d.ts +123 -0
  25. package/.nuxt/types/imports.d.ts +993 -0
  26. package/.nuxt/types/middleware.d.ts +17 -0
  27. package/.nuxt/types/nitro-config.d.ts +14 -0
  28. package/.nuxt/types/nitro-imports.d.ts +170 -0
  29. package/.nuxt/types/nitro-layouts.d.ts +17 -0
  30. package/.nuxt/types/nitro-nuxt.d.ts +39 -0
  31. package/.nuxt/types/nitro-routes.d.ts +17 -0
  32. package/.nuxt/types/nitro.d.ts +3 -0
  33. package/.nuxt/types/plugins.d.ts +43 -0
  34. package/.nuxt/types/schema.d.ts +213 -0
  35. package/.nuxt/types/vue-shim.d.ts +0 -0
  36. package/.trae/rules/rule.md +38 -0
  37. package/.vscode/settings.json +5 -0
  38. package/nuxt.config.ts +38 -0
  39. package/package.json +42 -0
  40. package/pnpm-lock.yaml +24 -0
  41. package/src/app.vue +46 -0
  42. package/src/components/AiWorkflowEditor.vue +142 -0
  43. package/src/components/ai/AiChatPanel.vue +135 -0
  44. package/src/components/ai/AiGenerator.vue +62 -0
  45. package/src/components/editor/Canvas.vue +175 -0
  46. package/src/components/editor/FormatPanel.vue +327 -0
  47. package/src/components/editor/NodePanel.vue +240 -0
  48. package/src/components/editor/PropertyPanel.vue +348 -0
  49. package/src/components/editor/Toolbar.vue +49 -0
  50. package/src/components/editor/nodes/AiNode.vue +77 -0
  51. package/src/components/editor/nodes/NormalNode.vue +75 -0
  52. package/src/components/editor/nodes/SysmlBlockNode.vue +72 -0
  53. package/src/components/editor/nodes/SysmlRequirementNode.vue +72 -0
  54. package/src/components/editor/nodes/SysmlUseCaseNode.vue +62 -0
  55. package/src/composables/useAi.ts +82 -0
  56. package/src/i18n.config.ts +5 -0
  57. package/src/locales/en.json +106 -0
  58. package/src/locales/zh.json +106 -0
  59. package/src/plugins/aiWorkflowEditor.ts +6 -0
  60. package/src/types/ai.ts +7 -0
  61. package/src/types/workflow.ts +25 -0
  62. package/src/utils/llmAdapter.ts +46 -0
  63. package/tsconfig.json +3 -0
  64. package/uno.config.ts +15 -0
  65. package//345/211/215/347/253/257/345/256/232/345/210/266/345/274/200/345/217/221/357/274/232ai-workflow-editor/345/274/200/345/217/221/350/256/241/345/210/222.md +655 -0
@@ -0,0 +1,348 @@
1
+ <template>
2
+ <div class="flex h-full flex-col bg-white border-l border-gray-200 w-80 shadow-[-4px_0_24px_rgba(0,0,0,0.08)] z-20 relative">
3
+ <!-- Header -->
4
+ <div class="flex items-center justify-between border-b border-gray-100 p-3 bg-gray-50/50">
5
+ <div class="font-bold text-gray-800">{{ $t('propertyPanel.title') }}</div>
6
+ <el-button link class="!text-gray-400 hover:!text-gray-600 !p-1" @click="$emit('close')">
7
+ <div class="i-lucide-x w-4 h-4" />
8
+ </el-button>
9
+ </div>
10
+
11
+ <!-- Tabs -->
12
+ <div class="flex border-b border-gray-200 bg-gray-50">
13
+ <button
14
+ v-for="tab in ['style', 'text', 'arrange']"
15
+ :key="tab"
16
+ @click="currentTab = tab"
17
+ :class="[
18
+ 'flex-1 py-2 text-xs font-medium transition-colors relative',
19
+ currentTab === tab ? 'text-gray-900 bg-white' : 'text-gray-500 hover:text-gray-700 hover:bg-gray-100'
20
+ ]"
21
+ >
22
+ {{ $t(`propertyPanel.tabs.${tab}`) }}
23
+ <div v-if="currentTab === tab" class="absolute bottom-0 left-0 right-0 h-0.5 bg-purple-600" />
24
+ </button>
25
+ </div>
26
+
27
+ <!-- Content -->
28
+ <div class="flex-1 overflow-y-auto p-4 custom-scrollbar">
29
+ <template v-if="selectedNode">
30
+
31
+ <!-- Style Tab -->
32
+ <div v-show="currentTab === 'style'" class="space-y-4">
33
+ <!-- Fill -->
34
+ <div>
35
+ <div class="flex items-center justify-between mb-2">
36
+ <el-checkbox v-model="hasFill" :label="$t('propertyPanel.style.fill')" />
37
+ <div class="flex items-center gap-2">
38
+ <el-color-picker v-model="nodeStyle.backgroundColor" show-alpha size="small" />
39
+ </div>
40
+ </div>
41
+ <!-- Presets -->
42
+ <div class="flex gap-1 flex-wrap mb-2">
43
+ <button
44
+ v-for="color in presetColors"
45
+ :key="color"
46
+ class="w-6 h-6 rounded border border-gray-200 cursor-pointer hover:scale-110 transition-transform"
47
+ :style="{ backgroundColor: color }"
48
+ @click="nodeStyle.backgroundColor = color"
49
+ />
50
+ </div>
51
+ </div>
52
+
53
+ <div class="h-px bg-gray-100" />
54
+
55
+ <!-- Stroke -->
56
+ <div>
57
+ <div class="flex items-center justify-between mb-2">
58
+ <el-checkbox v-model="hasStroke" :label="$t('propertyPanel.style.stroke')" />
59
+ <el-color-picker v-model="nodeStyle.borderColor" show-alpha size="small" />
60
+ </div>
61
+ <div class="grid grid-cols-2 gap-2">
62
+ <el-select v-model="nodeStyle.borderStyle" size="small">
63
+ <el-option value="solid" label="Solid" />
64
+ <el-option value="dashed" label="Dashed" />
65
+ <el-option value="dotted" label="Dotted" />
66
+ </el-select>
67
+ <el-input-number v-model="borderWidth" size="small" :min="0" :max="20" controls-position="right">
68
+ <template #suffix>pt</template>
69
+ </el-input-number>
70
+ </div>
71
+ </div>
72
+
73
+ <div class="h-px bg-gray-100" />
74
+
75
+ <!-- Opacity -->
76
+ <div class="flex items-center justify-between">
77
+ <span class="text-xs text-gray-500">{{ $t('propertyPanel.style.opacity') }}</span>
78
+ <el-input-number v-model="opacity" size="small" :min="0" :max="100" controls-position="right">
79
+ <template #suffix>%</template>
80
+ </el-input-number>
81
+ </div>
82
+
83
+ <!-- Radius & Shadow -->
84
+ <div class="grid grid-cols-2 gap-2">
85
+ <el-checkbox v-model="hasShadow" :label="$t('propertyPanel.style.shadow')" />
86
+ <el-checkbox v-model="isRounded" :label="$t('propertyPanel.style.rounded')" />
87
+ </div>
88
+ </div>
89
+
90
+ <!-- Text Tab -->
91
+ <div v-show="currentTab === 'text'" class="space-y-4">
92
+ <!-- Content -->
93
+ <div>
94
+ <label class="text-xs text-gray-500 mb-1 block">{{ $t('propertyPanel.text.content') }}</label>
95
+ <el-input v-model="nodeData.label" type="textarea" :rows="2" resize="none" />
96
+ </div>
97
+
98
+ <!-- Font -->
99
+ <div class="grid grid-cols-2 gap-2">
100
+ <el-select v-model="textStyle.fontFamily" size="small">
101
+ <el-option value="sans-serif" label="Sans Serif" />
102
+ <el-option value="serif" label="Serif" />
103
+ <el-option value="monospace" label="Monospace" />
104
+ </el-select>
105
+ <el-input-number v-model="fontSize" size="small" :min="8" :max="72" controls-position="right">
106
+ <template #suffix>px</template>
107
+ </el-input-number>
108
+ </div>
109
+
110
+ <!-- Style Buttons -->
111
+ <div class="flex bg-gray-100 rounded p-1 gap-1 justify-between">
112
+ <button
113
+ :class="['p-1 rounded hover:bg-white transition-colors', textStyle.fontWeight === 'bold' ? 'bg-white text-purple-600 shadow-sm' : 'text-gray-500']"
114
+ @click="toggleFontWeight"
115
+ >
116
+ <div class="i-lucide-bold w-4 h-4" />
117
+ </button>
118
+ <button
119
+ :class="['p-1 rounded hover:bg-white transition-colors', textStyle.fontStyle === 'italic' ? 'bg-white text-purple-600 shadow-sm' : 'text-gray-500']"
120
+ @click="toggleFontStyle"
121
+ >
122
+ <div class="i-lucide-italic w-4 h-4" />
123
+ </button>
124
+ <button
125
+ :class="['p-1 rounded hover:bg-white transition-colors', textStyle.textDecoration === 'underline' ? 'bg-white text-purple-600 shadow-sm' : 'text-gray-500']"
126
+ @click="toggleTextDecoration"
127
+ >
128
+ <div class="i-lucide-underline w-4 h-4" />
129
+ </button>
130
+ <div class="w-px bg-gray-300 my-0.5" />
131
+ <button
132
+ :class="['p-1 rounded hover:bg-white transition-colors', textStyle.textAlign === 'left' ? 'bg-white text-purple-600 shadow-sm' : 'text-gray-500']"
133
+ @click="textStyle.textAlign = 'left'"
134
+ >
135
+ <div class="i-lucide-align-left w-4 h-4" />
136
+ </button>
137
+ <button
138
+ :class="['p-1 rounded hover:bg-white transition-colors', textStyle.textAlign === 'center' ? 'bg-white text-purple-600 shadow-sm' : 'text-gray-500']"
139
+ @click="textStyle.textAlign = 'center'"
140
+ >
141
+ <div class="i-lucide-align-center w-4 h-4" />
142
+ </button>
143
+ <button
144
+ :class="['p-1 rounded hover:bg-white transition-colors', textStyle.textAlign === 'right' ? 'bg-white text-purple-600 shadow-sm' : 'text-gray-500']"
145
+ @click="textStyle.textAlign = 'right'"
146
+ >
147
+ <div class="i-lucide-align-right w-4 h-4" />
148
+ </button>
149
+ </div>
150
+
151
+ <!-- Color -->
152
+ <div class="flex items-center justify-between">
153
+ <el-checkbox v-model="hasTextColor" :label="$t('propertyPanel.text.color')" />
154
+ <el-color-picker v-model="textStyle.color" size="small" />
155
+ </div>
156
+ </div>
157
+
158
+ <!-- Arrange Tab -->
159
+ <div v-show="currentTab === 'arrange'" class="space-y-4">
160
+ <!-- Z-Order -->
161
+ <div class="grid grid-cols-2 gap-2">
162
+ <el-button size="small" @click="bringToFront">{{ $t('propertyPanel.arrange.front') }}</el-button>
163
+ <el-button size="small" @click="sendToBack">{{ $t('propertyPanel.arrange.back') }}</el-button>
164
+ </div>
165
+
166
+ <div class="h-px bg-gray-100" />
167
+
168
+ <!-- Size -->
169
+ <div>
170
+ <div class="text-xs font-bold text-gray-800 mb-2">{{ $t('propertyPanel.arrange.size') }}</div>
171
+ <div class="grid grid-cols-2 gap-2">
172
+ <el-input-number v-model="nodeWidth" size="small" controls-position="right">
173
+ <template #prefix>W</template>
174
+ </el-input-number>
175
+ <el-input-number v-model="nodeHeight" size="small" controls-position="right">
176
+ <template #prefix>H</template>
177
+ </el-input-number>
178
+ </div>
179
+ </div>
180
+
181
+ <!-- Position -->
182
+ <div>
183
+ <div class="text-xs font-bold text-gray-800 mb-2">{{ $t('propertyPanel.arrange.position') }}</div>
184
+ <div class="grid grid-cols-2 gap-2">
185
+ <el-input-number v-model="nodeX" size="small" controls-position="right">
186
+ <template #prefix>X</template>
187
+ </el-input-number>
188
+ <el-input-number v-model="nodeY" size="small" controls-position="right">
189
+ <template #prefix>Y</template>
190
+ </el-input-number>
191
+ </div>
192
+ </div>
193
+ </div>
194
+
195
+ </template>
196
+ <div v-else class="flex flex-col items-center justify-center h-full text-gray-400">
197
+ <div class="i-lucide-mouse-pointer-2 text-3xl mb-2" />
198
+ <span class="text-sm">{{ $t('propertyPanel.noSelection') }}</span>
199
+ </div>
200
+ </div>
201
+ </div>
202
+ </template>
203
+
204
+ <script setup lang="ts">
205
+ import { ref, computed, watch } from 'vue';
206
+ import { useVueFlow } from '@vue-flow/core';
207
+
208
+ const props = defineProps<{
209
+ selectedNode: any;
210
+ }>();
211
+
212
+ const emit = defineEmits(['close']);
213
+ const { applyNodeChanges } = useVueFlow();
214
+
215
+ const currentTab = ref('style');
216
+
217
+ // Presets
218
+ const presetColors = ['#ffffff', '#f8fafc', '#dbeafe', '#dcfce7', '#fef9c3', '#fae8ff', '#ffe4e6'];
219
+
220
+ // Computed proxies for Node Data
221
+ const nodeData = computed(() => props.selectedNode?.data || {});
222
+ const nodeStyle = computed(() => props.selectedNode?.data?.style || {});
223
+ const textStyle = computed(() => props.selectedNode?.data?.textStyle || {});
224
+ const position = computed(() => props.selectedNode?.position || { x: 0, y: 0 });
225
+ const dimensions = computed(() => props.selectedNode?.dimensions || { width: 0, height: 0 });
226
+
227
+ // Helper to init styles if missing
228
+ watch(() => props.selectedNode, (node) => {
229
+ if (node) {
230
+ if (!node.data.style) node.data.style = {};
231
+ if (!node.data.textStyle) node.data.textStyle = {
232
+ fontSize: '12px',
233
+ fontFamily: 'sans-serif',
234
+ color: '#374151',
235
+ textAlign: 'center'
236
+ };
237
+ }
238
+ }, { immediate: true });
239
+
240
+ // --- Style Getters/Setters ---
241
+ const hasFill = computed({
242
+ get: () => !!nodeStyle.value.backgroundColor && nodeStyle.value.backgroundColor !== 'transparent',
243
+ set: (val) => {
244
+ if (!val) nodeStyle.value.backgroundColor = 'transparent';
245
+ else nodeStyle.value.backgroundColor = '#ffffff';
246
+ }
247
+ });
248
+
249
+ const hasStroke = computed({
250
+ get: () => nodeStyle.value.borderWidth !== '0px',
251
+ set: (val) => {
252
+ if (!val) nodeStyle.value.borderWidth = '0px';
253
+ else nodeStyle.value.borderWidth = '1px';
254
+ }
255
+ });
256
+
257
+ const borderWidth = computed({
258
+ get: () => parseInt(nodeStyle.value.borderWidth || '0'),
259
+ set: (val) => nodeStyle.value.borderWidth = `${val}px`
260
+ });
261
+
262
+ const opacity = computed({
263
+ get: () => (nodeStyle.value.opacity ?? 1) * 100,
264
+ set: (val) => nodeStyle.value.opacity = val / 100
265
+ });
266
+
267
+ const hasShadow = computed({
268
+ get: () => !!nodeStyle.value.boxShadow,
269
+ set: (val) => {
270
+ if (val) nodeStyle.value.boxShadow = '0 4px 6px -1px rgba(0, 0, 0, 0.1)';
271
+ else nodeStyle.value.boxShadow = 'none';
272
+ }
273
+ });
274
+
275
+ const isRounded = computed({
276
+ get: () => nodeStyle.value.borderRadius !== '0px',
277
+ set: (val) => {
278
+ if (val) nodeStyle.value.borderRadius = '0.5rem';
279
+ else nodeStyle.value.borderRadius = '0px';
280
+ }
281
+ });
282
+
283
+ // --- Text Getters/Setters ---
284
+ const fontSize = computed({
285
+ get: () => parseInt(textStyle.value.fontSize || '12'),
286
+ set: (val) => textStyle.value.fontSize = `${val}px`
287
+ });
288
+
289
+ const hasTextColor = computed({
290
+ get: () => !!textStyle.value.color,
291
+ set: (val) => {
292
+ if (!val) textStyle.value.color = undefined;
293
+ else textStyle.value.color = '#374151';
294
+ }
295
+ });
296
+
297
+ const toggleFontWeight = () => {
298
+ textStyle.value.fontWeight = textStyle.value.fontWeight === 'bold' ? 'normal' : 'bold';
299
+ };
300
+ const toggleFontStyle = () => {
301
+ textStyle.value.fontStyle = textStyle.value.fontStyle === 'italic' ? 'normal' : 'italic';
302
+ };
303
+ const toggleTextDecoration = () => {
304
+ textStyle.value.textDecoration = textStyle.value.textDecoration === 'underline' ? 'none' : 'underline';
305
+ };
306
+
307
+ // --- Arrange Getters/Setters ---
308
+ const nodeX = computed({
309
+ get: () => Math.round(position.value.x),
310
+ set: (val) => props.selectedNode.position.x = val
311
+ });
312
+ const nodeY = computed({
313
+ get: () => Math.round(position.value.y),
314
+ set: (val) => props.selectedNode.position.y = val
315
+ });
316
+
317
+ // Resizing is tricky because Vue Flow nodes usually auto-size or use style width/height
318
+ // We will update the style width/height if set
319
+ const nodeWidth = computed({
320
+ get: () => parseInt(nodeStyle.value.width || dimensions.value.width || '160'),
321
+ set: (val) => nodeStyle.value.width = `${val}px`
322
+ });
323
+ const nodeHeight = computed({
324
+ get: () => parseInt(nodeStyle.value.height || dimensions.value.height || '40'),
325
+ set: (val) => nodeStyle.value.height = `${val}px`
326
+ });
327
+
328
+ const bringToFront = () => {
329
+ props.selectedNode.zIndex = (props.selectedNode.zIndex || 0) + 1;
330
+ };
331
+ const sendToBack = () => {
332
+ props.selectedNode.zIndex = (props.selectedNode.zIndex || 0) - 1;
333
+ };
334
+
335
+ </script>
336
+
337
+ <style scoped>
338
+ .custom-scrollbar::-webkit-scrollbar {
339
+ width: 4px;
340
+ }
341
+ .custom-scrollbar::-webkit-scrollbar-track {
342
+ background: transparent;
343
+ }
344
+ .custom-scrollbar::-webkit-scrollbar-thumb {
345
+ background: #e5e7eb;
346
+ border-radius: 4px;
347
+ }
348
+ </style>
@@ -0,0 +1,49 @@
1
+ <template>
2
+ <div class="h-12 border-b border-gray-200 bg-white/80 backdrop-blur-xl flex items-center justify-between px-4 shadow-sm z-30 relative">
3
+ <!-- Left: Actions -->
4
+ <div class="flex items-center gap-1">
5
+ <el-button text @click="console.log('Draw')">{{ $t('toolbar.draw') }}</el-button>
6
+ <el-button text @click="console.log('Shape')">{{ $t('toolbar.shape') }}</el-button>
7
+ <el-button text @click="console.log('Style')">{{ $t('toolbar.style') }}</el-button>
8
+ <el-button text @click="console.log('Insert')">{{ $t('toolbar.insert') }}</el-button>
9
+ <div class="w-px h-4 bg-gray-200 mx-2" />
10
+ <el-button text @click="$emit('save')" class="!p-1.5" :title="$t('toolbar.save')">
11
+ <div class="i-lucide-save w-4 h-4" />
12
+ </el-button>
13
+ <el-button text @click="$emit('import')" class="!p-1.5" :title="$t('toolbar.import')">
14
+ <div class="i-lucide-upload w-4 h-4" />
15
+ </el-button>
16
+ <div class="w-px h-4 bg-gray-200 mx-2" />
17
+ <el-button text class="!p-1.5" :title="$t('toolbar.undo')">
18
+ <div class="i-lucide-undo-2 w-4 h-4" />
19
+ </el-button>
20
+ <el-button text class="!p-1.5" :title="$t('toolbar.redo')">
21
+ <div class="i-lucide-redo-2 w-4 h-4" />
22
+ </el-button>
23
+ </div>
24
+
25
+ <!-- Center: Title/Status (Optional) -->
26
+ <div class="absolute left-1/2 -translate-x-1/2 text-sm font-medium text-gray-400 select-none pointer-events-none">
27
+ {{ $t('toolbar.untitled') }}
28
+ </div>
29
+
30
+ <!-- Right: Zoom/View -->
31
+ <div class="flex items-center gap-1 z-30">
32
+ <el-button text @click="$emit('zoom-out')" class="!p-1.5" :title="$t('toolbar.zoomOut')">
33
+ <div class="i-lucide-minus w-4 h-4" />
34
+ </el-button>
35
+ <span class="text-xs text-gray-500 w-8 text-center select-none">100%</span>
36
+ <el-button text @click="$emit('zoom-in')" class="!p-1.5" :title="$t('toolbar.zoomIn')">
37
+ <div class="i-lucide-plus w-4 h-4" />
38
+ </el-button>
39
+ <div class="w-px h-4 bg-gray-200 mx-2" />
40
+ <el-button text @click="$emit('fit-view')" class="!p-1.5" :title="$t('toolbar.fitView')">
41
+ <div class="i-lucide-maximize w-4 h-4" />
42
+ </el-button>
43
+ </div>
44
+ </div>
45
+ </template>
46
+
47
+ <script setup lang="ts">
48
+ defineEmits(['save', 'import', 'zoom-in', 'zoom-out', 'fit-view']);
49
+ </script>
@@ -0,0 +1,77 @@
1
+ <template>
2
+ <div
3
+ class="ai-node group relative min-w-[160px] h-full transition-all"
4
+ :style="containerStyle"
5
+ >
6
+ <NodeResizer :is-visible="selected" :min-width="100" :min-height="30" />
7
+ <Handle type="target" position="top" class="!bg-purple-500 !w-2.5 !h-2.5 !border-2 !border-white" />
8
+
9
+ <div class="flex items-center gap-3 h-full">
10
+ <div class="flex h-8 w-8 items-center justify-center rounded-md bg-purple-50 text-purple-600 group-hover:bg-purple-100 transition-colors">
11
+ <div class="i-lucide-brain-circuit text-lg" />
12
+ </div>
13
+ <div class="flex flex-col flex-1 min-w-0">
14
+ <span class="text-[10px] font-bold text-purple-600 uppercase tracking-wide leading-none mb-0.5">AI Node</span>
15
+ <textarea
16
+ v-if="data.isEditing"
17
+ v-model="data.label"
18
+ ref="textareaRef"
19
+ class="nodrag w-full bg-white border border-purple-300 rounded px-1 py-0.5 text-xs font-semibold text-gray-800 leading-tight focus:outline-none focus:ring-2 focus:ring-purple-200 resize-none overflow-hidden"
20
+ :style="data.textStyle"
21
+ rows="1"
22
+ @blur="stopEditing"
23
+ @keydown.enter.prevent="stopEditing"
24
+ @vue:mounted="focusInput"
25
+ />
26
+ <span
27
+ v-else
28
+ class="text-xs font-semibold text-gray-800 leading-tight break-words whitespace-pre-wrap"
29
+ :style="data.textStyle"
30
+ >{{ data.label }}</span>
31
+ </div>
32
+ </div>
33
+
34
+ <!-- Status indicator (optional) -->
35
+ <div class="absolute -top-1 -right-1 h-2 w-2 rounded-full bg-green-500 border border-white opacity-0 group-hover:opacity-100 transition-opacity" />
36
+
37
+ <Handle type="source" position="bottom" class="!bg-purple-500 !w-2.5 !h-2.5 !border-2 !border-white" />
38
+ </div>
39
+ </template>
40
+
41
+ <script setup lang="ts">
42
+ import { computed, ref, nextTick } from 'vue';
43
+ import { Handle } from '@vue-flow/core';
44
+ import { NodeResizer } from '@vue-flow/node-resizer';
45
+ import '@vue-flow/node-resizer/dist/style.css';
46
+
47
+ const props = defineProps<{
48
+ data: { label: string; style?: any; textStyle?: any; isEditing?: boolean };
49
+ selected?: boolean;
50
+ }>();
51
+
52
+ const textareaRef = ref<HTMLTextAreaElement>();
53
+
54
+ const focusInput = () => {
55
+ nextTick(() => {
56
+ textareaRef.value?.focus();
57
+ textareaRef.value?.select();
58
+ });
59
+ };
60
+
61
+ const stopEditing = () => {
62
+ if (props.data) {
63
+ props.data.isEditing = false;
64
+ }
65
+ };
66
+
67
+ const containerStyle = computed(() => ({
68
+ backgroundColor: props.data.style?.backgroundColor || '#ffffff',
69
+ borderColor: props.selected ? '#a855f7' : (props.data.style?.borderColor || '#e5e7eb'),
70
+ borderWidth: props.data.style?.borderWidth || '1px',
71
+ borderStyle: props.data.style?.borderStyle || 'solid',
72
+ borderRadius: props.data.style?.borderRadius || '0.5rem',
73
+ boxShadow: props.selected ? '0 4px 6px -1px rgba(168, 85, 247, 0.2)' : (props.data.style?.boxShadow || '0 1px 2px 0 rgba(0, 0, 0, 0.05)'),
74
+ opacity: props.data.style?.opacity !== undefined ? props.data.style.opacity : 1,
75
+ padding: '0.75rem',
76
+ }));
77
+ </script>
@@ -0,0 +1,75 @@
1
+ <template>
2
+ <div
3
+ class="normal-node group relative min-w-[160px] h-full transition-all"
4
+ :style="containerStyle"
5
+ >
6
+ <NodeResizer :is-visible="selected" :min-width="100" :min-height="30" />
7
+ <Handle type="target" position="top" class="!bg-blue-500 !w-2.5 !h-2.5 !border-2 !border-white" />
8
+
9
+ <div class="flex items-center gap-3 h-full">
10
+ <div class="flex h-8 w-8 items-center justify-center rounded-md bg-blue-50 text-blue-600 group-hover:bg-blue-100 transition-colors">
11
+ <div class="i-lucide-square-activity text-lg" />
12
+ </div>
13
+ <div class="flex flex-col flex-1 min-w-0">
14
+ <span class="text-[10px] font-bold text-blue-600 uppercase tracking-wide leading-none mb-0.5">Action</span>
15
+
16
+ <textarea
17
+ v-if="data.isEditing"
18
+ v-model="data.label"
19
+ ref="textareaRef"
20
+ class="nodrag w-full bg-white border border-blue-300 rounded px-1 py-0.5 text-xs font-semibold text-gray-800 leading-tight focus:outline-none focus:ring-2 focus:ring-blue-200 resize-none overflow-hidden"
21
+ :style="data.textStyle"
22
+ rows="1"
23
+ @blur="stopEditing"
24
+ @keydown.enter.prevent="stopEditing"
25
+ @vue:mounted="focusInput"
26
+ />
27
+ <span
28
+ v-else
29
+ class="text-xs font-semibold text-gray-800 leading-tight break-words whitespace-pre-wrap"
30
+ :style="data.textStyle"
31
+ >{{ data.label }}</span>
32
+ </div>
33
+ </div>
34
+
35
+ <Handle type="source" position="bottom" class="!bg-blue-500 !w-2.5 !h-2.5 !border-2 !border-white" />
36
+ </div>
37
+ </template>
38
+
39
+ <script setup lang="ts">
40
+ import { computed, ref, nextTick } from 'vue';
41
+ import { Handle } from '@vue-flow/core';
42
+ import { NodeResizer } from '@vue-flow/node-resizer';
43
+ import '@vue-flow/node-resizer/dist/style.css';
44
+
45
+ const props = defineProps<{
46
+ data: { label: string; style?: any; textStyle?: any; isEditing?: boolean };
47
+ selected?: boolean;
48
+ }>();
49
+
50
+ const textareaRef = ref<HTMLTextAreaElement>();
51
+
52
+ const focusInput = () => {
53
+ nextTick(() => {
54
+ textareaRef.value?.focus();
55
+ textareaRef.value?.select();
56
+ });
57
+ };
58
+
59
+ const stopEditing = () => {
60
+ if (props.data) {
61
+ props.data.isEditing = false;
62
+ }
63
+ };
64
+
65
+ const containerStyle = computed(() => ({
66
+ backgroundColor: props.data.style?.backgroundColor || '#ffffff',
67
+ borderColor: props.selected ? '#60a5fa' : (props.data.style?.borderColor || '#e5e7eb'),
68
+ borderWidth: props.data.style?.borderWidth || '1px',
69
+ borderStyle: props.data.style?.borderStyle || 'solid',
70
+ borderRadius: props.data.style?.borderRadius || '0.5rem',
71
+ boxShadow: props.selected ? '0 4px 6px -1px rgba(96, 165, 250, 0.2)' : (props.data.style?.boxShadow || '0 1px 2px 0 rgba(0, 0, 0, 0.05)'),
72
+ opacity: props.data.style?.opacity !== undefined ? props.data.style.opacity : 1,
73
+ padding: '0.75rem',
74
+ }));
75
+ </script>
@@ -0,0 +1,72 @@
1
+ <template>
2
+ <div class="sysml-block-node group relative min-w-[160px] h-full transition-all" :style="containerStyle">
3
+ <NodeResizer :is-visible="selected" :min-width="100" :min-height="60" />
4
+ <Handle type="target" position="top" class="!bg-gray-700 !w-2.5 !h-2.5 !border-2 !border-white" />
5
+
6
+ <div class="flex flex-col h-full">
7
+ <!-- Stereotype -->
8
+ <div class="px-2 pt-1 text-center text-[10px] italic text-gray-500 font-mono">
9
+ &laquo;block&raquo;
10
+ </div>
11
+ <!-- Name -->
12
+ <div v-if="data.isEditing" class="px-2 pb-2 border-b border-gray-300">
13
+ <input
14
+ v-model="data.label"
15
+ ref="inputRef"
16
+ class="nodrag w-full text-center text-sm font-bold text-gray-800 bg-white border border-blue-300 rounded px-1 focus:outline-none focus:ring-2 focus:ring-blue-200"
17
+ :style="data.textStyle"
18
+ @blur="stopEditing"
19
+ @keydown.enter.prevent="stopEditing"
20
+ @vue:mounted="focusInput"
21
+ />
22
+ </div>
23
+ <div v-else class="px-2 pb-2 text-center text-sm font-bold text-gray-800 border-b border-gray-300 break-words" :style="data.textStyle">
24
+ {{ data.label }}
25
+ </div>
26
+ <!-- Attributes/Operations Placeholder -->
27
+ <div class="p-2 min-h-[40px] text-[10px] text-gray-400 flex-1">
28
+ <div class="italic">properties...</div>
29
+ <div class="italic mt-1">operations...</div>
30
+ </div>
31
+ </div>
32
+
33
+ <Handle type="source" position="bottom" class="!bg-gray-700 !w-2.5 !h-2.5 !border-2 !border-white" />
34
+ </div>
35
+ </template>
36
+
37
+ <script setup lang="ts">
38
+ import { computed, ref, nextTick } from 'vue';
39
+ import { Handle } from '@vue-flow/core';
40
+ import { NodeResizer } from '@vue-flow/node-resizer';
41
+ import '@vue-flow/node-resizer/dist/style.css';
42
+
43
+ const props = defineProps<{
44
+ data: { label: string; style?: any; textStyle?: any; isEditing?: boolean };
45
+ selected?: boolean;
46
+ }>();
47
+
48
+ const inputRef = ref<HTMLInputElement>();
49
+
50
+ const focusInput = () => {
51
+ nextTick(() => {
52
+ inputRef.value?.focus();
53
+ inputRef.value?.select();
54
+ });
55
+ };
56
+
57
+ const stopEditing = () => {
58
+ if (props.data) {
59
+ props.data.isEditing = false;
60
+ }
61
+ };
62
+
63
+ const containerStyle = computed(() => ({
64
+ backgroundColor: props.data.style?.backgroundColor || '#ffffff',
65
+ borderColor: props.selected ? '#3b82f6' : (props.data.style?.borderColor || '#374151'),
66
+ borderWidth: props.data.style?.borderWidth || '2px',
67
+ borderStyle: props.data.style?.borderStyle || 'solid',
68
+ borderRadius: props.data.style?.borderRadius || '0.125rem',
69
+ boxShadow: props.selected ? '0 4px 6px -1px rgba(59, 130, 246, 0.2)' : (props.data.style?.boxShadow || '0 1px 2px 0 rgba(0, 0, 0, 0.05)'),
70
+ opacity: props.data.style?.opacity !== undefined ? props.data.style.opacity : 1,
71
+ }));
72
+ </script>