@d34dman/flowdrop 0.0.35 → 0.0.37

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.
@@ -34,6 +34,7 @@
34
34
  invalidateSchemaCache,
35
35
  type DynamicSchemaResult
36
36
  } from '../services/dynamicSchemaService.js';
37
+ import { globalSaveWorkflow } from '../services/globalSave.js';
37
38
 
38
39
  interface Props {
39
40
  /** Optional workflow node (if provided, schema and values are derived from it) */
@@ -46,6 +47,8 @@
46
47
  showUIExtensions?: boolean;
47
48
  /** Optional workflow ID for context in external links */
48
49
  workflowId?: string;
50
+ /** Whether to also save the workflow when saving config */
51
+ saveWorkflowWhenSavingConfig?: boolean;
49
52
  /** Callback when form is saved (includes both config and extensions if enabled) */
50
53
  onSave: (config: Record<string, unknown>, uiExtensions?: NodeUIExtensions) => void;
51
54
  /** Callback when form is cancelled */
@@ -58,6 +61,7 @@
58
61
  values,
59
62
  showUIExtensions = true,
60
63
  workflowId,
64
+ saveWorkflowWhenSavingConfig = false,
61
65
  onSave,
62
66
  onCancel
63
67
  }: Props = $props();
@@ -137,6 +141,11 @@
137
141
  */
138
142
  let uiExtensionValues = $state<NodeUIExtensions>({});
139
143
 
144
+ /**
145
+ * Flag to track if workflow save is in progress
146
+ */
147
+ let isSavingWorkflow = $state(false);
148
+
140
149
  /**
141
150
  * Get initial UI extensions from node (instance level overrides type level)
142
151
  */
@@ -270,8 +279,9 @@
270
279
  /**
271
280
  * Handle form submission
272
281
  * Collects both config values and UI extension values
282
+ * Optionally saves the workflow if the option is enabled
273
283
  */
274
- function handleSave(): void {
284
+ async function handleSave(): Promise<void> {
275
285
  // Collect all form values including hidden fields
276
286
  const form = document.querySelector('.config-form');
277
287
  const updatedConfig: Record<string, unknown> = { ...configValues };
@@ -322,6 +332,18 @@
322
332
  } else {
323
333
  onSave(updatedConfig);
324
334
  }
335
+
336
+ // Save workflow if the option is enabled
337
+ if (saveWorkflowWhenSavingConfig) {
338
+ isSavingWorkflow = true;
339
+ try {
340
+ await globalSaveWorkflow();
341
+ } catch (error) {
342
+ console.error('Failed to save workflow after config save:', error);
343
+ } finally {
344
+ isSavingWorkflow = false;
345
+ }
346
+ }
325
347
  }
326
348
 
327
349
  /**
@@ -498,13 +520,23 @@
498
520
  type="button"
499
521
  class="config-form__button config-form__button--secondary"
500
522
  onclick={onCancel}
523
+ disabled={isSavingWorkflow}
501
524
  >
502
525
  <Icon icon="heroicons:x-mark" class="config-form__button-icon" />
503
526
  <span>Cancel</span>
504
527
  </button>
505
- <button type="submit" class="config-form__button config-form__button--primary">
506
- <Icon icon="heroicons:check" class="config-form__button-icon" />
507
- <span>Save Changes</span>
528
+ <button
529
+ type="submit"
530
+ class="config-form__button config-form__button--primary"
531
+ disabled={isSavingWorkflow}
532
+ >
533
+ {#if isSavingWorkflow}
534
+ <span class="config-form__button-spinner"></span>
535
+ <span>Saving...</span>
536
+ {:else}
537
+ <Icon icon="heroicons:check" class="config-form__button-icon" />
538
+ <span>Save Changes</span>
539
+ {/if}
508
540
  </button>
509
541
  </div>
510
542
  </form>
@@ -560,6 +592,16 @@
560
592
  margin-top: 0.5rem;
561
593
  }
562
594
 
595
+ /* Button Spinner */
596
+ .config-form__button-spinner {
597
+ width: 1rem;
598
+ height: 1rem;
599
+ border: 2px solid rgba(255, 255, 255, 0.3);
600
+ border-top-color: #ffffff;
601
+ border-radius: 50%;
602
+ animation: config-form-spin 0.6s linear infinite;
603
+ }
604
+
563
605
  .config-form__button {
564
606
  display: inline-flex;
565
607
  align-items: center;
@@ -10,6 +10,8 @@ interface Props {
10
10
  showUIExtensions?: boolean;
11
11
  /** Optional workflow ID for context in external links */
12
12
  workflowId?: string;
13
+ /** Whether to also save the workflow when saving config */
14
+ saveWorkflowWhenSavingConfig?: boolean;
13
15
  /** Callback when form is saved (includes both config and extensions if enabled) */
14
16
  onSave: (config: Record<string, unknown>, uiExtensions?: NodeUIExtensions) => void;
15
17
  /** Callback when form is cancelled */
@@ -16,8 +16,13 @@
16
16
 
17
17
  <script lang="ts">
18
18
  import { onMount, onDestroy } from 'svelte';
19
- import { EditorView, basicSetup } from 'codemirror';
19
+ import { EditorView, lineNumbers, highlightActiveLineGutter, drawSelection } from '@codemirror/view';
20
20
  import { EditorState } from '@codemirror/state';
21
+ import { history, historyKeymap } from '@codemirror/commands';
22
+ import { highlightSpecialChars, highlightActiveLine } from '@codemirror/view';
23
+ import { syntaxHighlighting, defaultHighlightStyle, indentOnInput } from '@codemirror/language';
24
+ import { keymap } from '@codemirror/view';
25
+ import { defaultKeymap, indentWithTab } from '@codemirror/commands';
21
26
  import { json, jsonParseLinter } from '@codemirror/lang-json';
22
27
  import { oneDark } from '@codemirror/theme-one-dark';
23
28
  import { linter, lintGutter } from '@codemirror/lint';
@@ -156,14 +161,36 @@
156
161
 
157
162
  /**
158
163
  * Create editor extensions array
164
+ * Uses minimal setup for better performance (no auto-closing brackets, no autocompletion)
159
165
  */
160
166
  function createExtensions() {
161
167
  const extensions = [
162
- basicSetup,
168
+ // Essential visual features
169
+ lineNumbers(),
170
+ highlightActiveLineGutter(),
171
+ highlightSpecialChars(),
172
+ highlightActiveLine(),
173
+ drawSelection(),
174
+
175
+ // Editing features
176
+ history(),
177
+ indentOnInput(),
178
+
179
+ // Syntax highlighting
180
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
181
+
182
+ // Keymaps for basic editing
183
+ keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
184
+
185
+ // JSON-specific features
163
186
  json(),
164
187
  linter(jsonParseLinter()),
165
188
  lintGutter(),
189
+
190
+ // Update listener
166
191
  EditorView.updateListener.of(handleUpdate),
192
+
193
+ // Custom theme
167
194
  EditorView.theme({
168
195
  '&': {
169
196
  height: height,
@@ -16,15 +16,23 @@
16
16
 
17
17
  <script lang="ts">
18
18
  import { onMount, onDestroy } from 'svelte';
19
- import { EditorView, basicSetup } from 'codemirror';
20
- import { EditorState } from '@codemirror/state';
21
19
  import {
20
+ EditorView,
21
+ lineNumbers,
22
+ highlightActiveLineGutter,
23
+ drawSelection,
24
+ highlightSpecialChars,
25
+ highlightActiveLine,
26
+ keymap,
22
27
  Decoration,
23
28
  type DecorationSet,
24
29
  ViewPlugin,
25
30
  type ViewUpdate,
26
31
  MatchDecorator
27
32
  } from '@codemirror/view';
33
+ import { EditorState } from '@codemirror/state';
34
+ import { history, historyKeymap, defaultKeymap, indentWithTab } from '@codemirror/commands';
35
+ import { syntaxHighlighting, defaultHighlightStyle, indentOnInput } from '@codemirror/language';
28
36
  import { oneDark } from '@codemirror/theme-one-dark';
29
37
 
30
38
  interface Props {
@@ -114,12 +122,34 @@
114
122
 
115
123
  /**
116
124
  * Create editor extensions array for template editing
125
+ * Uses minimal setup for better performance (no auto-closing brackets, no autocompletion)
117
126
  */
118
127
  function createExtensions() {
119
128
  const extensions = [
120
- basicSetup,
129
+ // Essential visual features
130
+ lineNumbers(),
131
+ highlightActiveLineGutter(),
132
+ highlightSpecialChars(),
133
+ highlightActiveLine(),
134
+ drawSelection(),
135
+
136
+ // Editing features
137
+ history(),
138
+ indentOnInput(),
139
+
140
+ // Syntax highlighting
141
+ syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
142
+
143
+ // Keymaps for basic editing
144
+ keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
145
+
146
+ // Template-specific variable highlighter
121
147
  variableHighlighter,
148
+
149
+ // Update listener
122
150
  EditorView.updateListener.of(handleUpdate),
151
+
152
+ // Custom theme
123
153
  EditorView.theme({
124
154
  '&': {
125
155
  height: height,
@@ -26,6 +26,24 @@
26
26
 
27
27
  let props: Props = $props();
28
28
 
29
+ /**
30
+ * Instance-specific title override from config.
31
+ * Falls back to the original label if not set.
32
+ * This allows users to customize the node title per-instance via config.
33
+ */
34
+ const displayTitle = $derived(
35
+ (props.data.config?.instanceTitle as string) || props.data.label
36
+ );
37
+
38
+ /**
39
+ * Instance-specific description override from config.
40
+ * Falls back to the metadata description if not set.
41
+ * This allows users to customize the node description per-instance via config.
42
+ */
43
+ const displayDescription = $derived(
44
+ (props.data.config?.instanceDescription as string) || props.data.metadata.description
45
+ );
46
+
29
47
  /**
30
48
  * Get the hideUnconnectedHandles setting from extensions
31
49
  * Merges node type defaults with instance overrides
@@ -137,7 +155,7 @@
137
155
  onkeydown={handleKeydown}
138
156
  role="button"
139
157
  tabindex="0"
140
- aria-label="Gateway node: {props.data.metadata.name}"
158
+ aria-label="Gateway node: {displayTitle}"
141
159
  aria-describedby="node-description-{props.data.nodeId || 'unknown'}"
142
160
  >
143
161
  <!-- Node Header -->
@@ -151,17 +169,17 @@
151
169
  <Icon icon={getNodeIcon(props.data.metadata.icon, props.data.metadata.category)} />
152
170
  </div>
153
171
 
154
- <!-- Node Title -->
172
+ <!-- Node Title - uses instanceTitle override if set -->
155
173
  <h3 class="flowdrop-text--sm flowdrop-font--medium flowdrop-truncate flowdrop-flex--1">
156
- {props.data.label}
174
+ {displayTitle}
157
175
  </h3>
158
176
  </div>
159
- <!-- Node Description -->
177
+ <!-- Node Description - uses instanceDescription override if set -->
160
178
  <p
161
179
  class="flowdrop-text--xs flowdrop-text--gray flowdrop-truncate flowdrop-mt--1"
162
180
  id="node-description-{props.data.nodeId || 'unknown'}"
163
181
  >
164
- {props.data.metadata.description}
182
+ {displayDescription}
165
183
  </p>
166
184
  </div>
167
185
 
@@ -0,0 +1,490 @@
1
+ <!--
2
+ Idea Node Component
3
+ A BPMN-like conceptual flow node with card design and configurable ports.
4
+ Allows users to create and chain ideas together without committing to specific node types.
5
+ Supports 4 connection points: left, right, top, and bottom (configurable via checkboxes).
6
+ Styled with BEM syntax
7
+ -->
8
+
9
+ <script lang="ts">
10
+ import { Position, Handle } from "@xyflow/svelte";
11
+ import type { ConfigValues, NodeMetadata } from "../../types/index.js";
12
+ import Icon from "@iconify/svelte";
13
+ import { getDataTypeColor } from "../../utils/colors.js";
14
+
15
+ /**
16
+ * IdeaNode component props
17
+ * Displays a card-style node for conceptual flow diagrams
18
+ */
19
+ const props = $props<{
20
+ data: {
21
+ label: string;
22
+ config: ConfigValues;
23
+ metadata: NodeMetadata;
24
+ nodeId?: string;
25
+ onConfigOpen?: (node: {
26
+ id: string;
27
+ type: string;
28
+ data: { label: string; config: ConfigValues; metadata: NodeMetadata };
29
+ }) => void;
30
+ };
31
+ selected?: boolean;
32
+ isProcessing?: boolean;
33
+ isError?: boolean;
34
+ }>();
35
+
36
+ /**
37
+ * Instance-specific title override from config.
38
+ * Falls back to the original label if not set.
39
+ * This allows users to customize the node title per-instance via config.
40
+ * Note: Also supports legacy 'title' property for backward compatibility.
41
+ */
42
+ const displayTitle = $derived(
43
+ (props.data.config?.instanceTitle as string) ||
44
+ (props.data.config?.title as string) ||
45
+ props.data.label ||
46
+ props.data.metadata?.name ||
47
+ "New Idea"
48
+ );
49
+
50
+ /**
51
+ * Instance-specific description override from config.
52
+ * Falls back to the metadata description if not set.
53
+ * This allows users to customize the node description per-instance via config.
54
+ * Note: Also supports legacy 'description' property for backward compatibility.
55
+ */
56
+ const displayDescription = $derived(
57
+ (props.data.config?.instanceDescription as string) ||
58
+ (props.data.config?.description as string) ||
59
+ props.data.metadata?.description ||
60
+ "Click to add description..."
61
+ );
62
+
63
+ /**
64
+ * Get custom icon from config or metadata, with fallback
65
+ */
66
+ const ideaIcon = $derived(
67
+ (props.data.config?.icon as string) ||
68
+ (props.data.metadata?.icon as string) ||
69
+ "mdi:lightbulb-outline"
70
+ );
71
+
72
+ /**
73
+ * Get accent color from config or metadata, with fallback
74
+ */
75
+ const ideaColor = $derived(
76
+ (props.data.config?.color as string) ||
77
+ (props.data.metadata?.color as string) ||
78
+ "#6366f1"
79
+ );
80
+
81
+ /**
82
+ * Port visibility configuration from config
83
+ * Left and Right are enabled by default, Top and Bottom are disabled by default
84
+ */
85
+ const enableLeftPort = $derived(
86
+ (props.data.config?.enableLeftPort as boolean) ?? true
87
+ );
88
+ const enableRightPort = $derived(
89
+ (props.data.config?.enableRightPort as boolean) ?? true
90
+ );
91
+ const enableTopPort = $derived(
92
+ (props.data.config?.enableTopPort as boolean) ?? false
93
+ );
94
+ const enableBottomPort = $derived(
95
+ (props.data.config?.enableBottomPort as boolean) ?? false
96
+ );
97
+
98
+ /**
99
+ * Data type for idea flow connections
100
+ */
101
+ const IDEA_DATA_TYPE = "idea";
102
+
103
+ /**
104
+ * Opens the configuration sidebar for editing idea properties
105
+ */
106
+ function openConfigSidebar(): void {
107
+ if (props.data.onConfigOpen) {
108
+ const nodeForConfig = {
109
+ id: props.data.nodeId || "unknown",
110
+ type: "idea",
111
+ data: props.data
112
+ };
113
+ props.data.onConfigOpen(nodeForConfig);
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Handles double-click to open config sidebar
119
+ */
120
+ function handleDoubleClick(): void {
121
+ openConfigSidebar();
122
+ }
123
+
124
+ /**
125
+ * Handle single click - selection handled by SvelteFlow
126
+ */
127
+ function handleClick(): void {
128
+ // Node selection is handled by Svelte Flow
129
+ }
130
+
131
+ /**
132
+ * Handles keyboard events for accessibility
133
+ * @param event - The keyboard event
134
+ */
135
+ function handleKeydown(event: KeyboardEvent): void {
136
+ if (event.key === "Enter" || event.key === " ") {
137
+ event.preventDefault();
138
+ handleDoubleClick();
139
+ }
140
+ }
141
+ </script>
142
+
143
+ <!-- Idea Node -->
144
+ <div
145
+ class="flowdrop-idea-node"
146
+ class:flowdrop-idea-node--selected={props.selected}
147
+ class:flowdrop-idea-node--processing={props.isProcessing}
148
+ class:flowdrop-idea-node--error={props.isError}
149
+ style="--idea-accent-color: {ideaColor};"
150
+ onclick={handleClick}
151
+ ondblclick={handleDoubleClick}
152
+ onkeydown={handleKeydown}
153
+ role="button"
154
+ tabindex="0"
155
+ aria-label="Idea node: {displayTitle}"
156
+ >
157
+ <!-- Left Port (Target/Input) -->
158
+ {#if enableLeftPort}
159
+ <Handle
160
+ type="target"
161
+ position={Position.Left}
162
+ style="background-color: {getDataTypeColor(IDEA_DATA_TYPE)}; border-color: #ffffff; top: 50%; transform: translateY(-50%); z-index: 30;"
163
+ id={`${props.data.nodeId}-input-left`}
164
+ />
165
+ {/if}
166
+
167
+ <!-- Top Port (Target/Input) -->
168
+ {#if enableTopPort}
169
+ <Handle
170
+ type="target"
171
+ position={Position.Top}
172
+ style="background-color: {getDataTypeColor(IDEA_DATA_TYPE)}; border-color: #ffffff; left: 50%; transform: translateX(-50%); z-index: 30;"
173
+ id={`${props.data.nodeId}-input-top`}
174
+ />
175
+ {/if}
176
+
177
+ <!-- Card Content -->
178
+ <div class="flowdrop-idea-node__card">
179
+ <!-- Accent Bar -->
180
+ <div class="flowdrop-idea-node__accent-bar"></div>
181
+
182
+ <!-- Header with icon and title -->
183
+ <div class="flowdrop-idea-node__header">
184
+ <div class="flowdrop-idea-node__icon-wrapper">
185
+ <Icon icon={ideaIcon} class="flowdrop-idea-node__icon" />
186
+ </div>
187
+ <h3 class="flowdrop-idea-node__title">{displayTitle}</h3>
188
+ </div>
189
+
190
+ <!-- Description Body -->
191
+ <div class="flowdrop-idea-node__body">
192
+ <p class="flowdrop-idea-node__description">{displayDescription}</p>
193
+ </div>
194
+
195
+ <!-- Processing indicator -->
196
+ {#if props.isProcessing}
197
+ <div class="flowdrop-idea-node__processing">
198
+ <div class="flowdrop-idea-node__spinner"></div>
199
+ <span>Processing...</span>
200
+ </div>
201
+ {/if}
202
+
203
+ <!-- Error indicator -->
204
+ {#if props.isError}
205
+ <div class="flowdrop-idea-node__error">
206
+ <Icon icon="mdi:alert-circle" class="flowdrop-idea-node__error-icon" />
207
+ <span>Error</span>
208
+ </div>
209
+ {/if}
210
+ </div>
211
+
212
+ <!-- Config button -->
213
+ <button
214
+ class="flowdrop-idea-node__config-btn"
215
+ onclick={openConfigSidebar}
216
+ title="Configure idea"
217
+ >
218
+ <Icon icon="mdi:cog" />
219
+ </button>
220
+
221
+ <!-- Right Port (Source/Output) -->
222
+ {#if enableRightPort}
223
+ <Handle
224
+ type="source"
225
+ position={Position.Right}
226
+ style="background-color: {getDataTypeColor(IDEA_DATA_TYPE)}; border-color: #ffffff; top: 50%; transform: translateY(-50%); z-index: 30;"
227
+ id={`${props.data.nodeId}-output-right`}
228
+ />
229
+ {/if}
230
+
231
+ <!-- Bottom Port (Source/Output) -->
232
+ {#if enableBottomPort}
233
+ <Handle
234
+ type="source"
235
+ position={Position.Bottom}
236
+ style="background-color: {getDataTypeColor(IDEA_DATA_TYPE)}; border-color: #ffffff; left: 50%; transform: translateX(-50%); z-index: 30;"
237
+ id={`${props.data.nodeId}-output-bottom`}
238
+ />
239
+ {/if}
240
+ </div>
241
+
242
+ <style>
243
+ .flowdrop-idea-node {
244
+ position: relative;
245
+ width: 18rem;
246
+ cursor: pointer;
247
+ transition: all 0.2s ease-in-out;
248
+ z-index: 10;
249
+ }
250
+
251
+ .flowdrop-idea-node__card {
252
+ background-color: #ffffff;
253
+ border-radius: 0.75rem;
254
+ border: 1px solid #e5e7eb;
255
+ box-shadow:
256
+ 0 4px 6px -1px rgba(0, 0, 0, 0.1),
257
+ 0 2px 4px -1px rgba(0, 0, 0, 0.06);
258
+ overflow: hidden;
259
+ transition: all 0.2s ease-in-out;
260
+ }
261
+
262
+ .flowdrop-idea-node:hover .flowdrop-idea-node__card {
263
+ box-shadow:
264
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1),
265
+ 0 4px 6px -2px rgba(0, 0, 0, 0.05);
266
+ transform: translateY(-1px);
267
+ }
268
+
269
+ .flowdrop-idea-node--selected .flowdrop-idea-node__card {
270
+ border-color: #3b82f6;
271
+ box-shadow:
272
+ 0 10px 15px -3px rgba(0, 0, 0, 0.1),
273
+ 0 0 0 3px rgba(59, 130, 246, 0.3);
274
+ }
275
+
276
+ .flowdrop-idea-node--processing .flowdrop-idea-node__card {
277
+ opacity: 0.8;
278
+ }
279
+
280
+ .flowdrop-idea-node--error .flowdrop-idea-node__card {
281
+ border-color: #ef4444 !important;
282
+ background-color: #fef2f2 !important;
283
+ }
284
+
285
+ /* Accent bar at top of card */
286
+ .flowdrop-idea-node__accent-bar {
287
+ height: 4px;
288
+ background-color: var(--idea-accent-color, #6366f1);
289
+ transition: background-color 0.2s ease-in-out;
290
+ }
291
+
292
+ /* Header section */
293
+ .flowdrop-idea-node__header {
294
+ display: flex;
295
+ align-items: center;
296
+ gap: 0.625rem;
297
+ padding: 0.75rem 1rem 0.5rem;
298
+ }
299
+
300
+ .flowdrop-idea-node__icon-wrapper {
301
+ display: flex;
302
+ align-items: center;
303
+ justify-content: center;
304
+ width: 2rem;
305
+ height: 2rem;
306
+ background-color: color-mix(in srgb, var(--idea-accent-color, #6366f1) 15%, transparent);
307
+ border-radius: 0.5rem;
308
+ flex-shrink: 0;
309
+ }
310
+
311
+ :global(.flowdrop-idea-node__icon) {
312
+ width: 1.25rem;
313
+ height: 1.25rem;
314
+ color: var(--idea-accent-color, #6366f1);
315
+ }
316
+
317
+ .flowdrop-idea-node__title {
318
+ font-size: 0.9375rem;
319
+ font-weight: 600;
320
+ color: #1f2937;
321
+ margin: 0;
322
+ line-height: 1.3;
323
+ overflow: hidden;
324
+ text-overflow: ellipsis;
325
+ white-space: nowrap;
326
+ }
327
+
328
+ /* Body section */
329
+ .flowdrop-idea-node__body {
330
+ padding: 0 1rem 0.875rem;
331
+ }
332
+
333
+ .flowdrop-idea-node__description {
334
+ font-size: 0.8125rem;
335
+ color: #6b7280;
336
+ margin: 0;
337
+ line-height: 1.5;
338
+ display: -webkit-box;
339
+ -webkit-line-clamp: 3;
340
+ -webkit-box-orient: vertical;
341
+ overflow: hidden;
342
+ }
343
+
344
+ /* Processing indicator */
345
+ .flowdrop-idea-node__processing {
346
+ display: flex;
347
+ align-items: center;
348
+ gap: 0.5rem;
349
+ padding: 0.5rem 1rem;
350
+ font-size: 0.75rem;
351
+ color: #6b7280;
352
+ border-top: 1px solid #f3f4f6;
353
+ }
354
+
355
+ .flowdrop-idea-node__spinner {
356
+ width: 0.875rem;
357
+ height: 0.875rem;
358
+ border: 2px solid #e5e7eb;
359
+ border-top-color: var(--idea-accent-color, #6366f1);
360
+ border-radius: 50%;
361
+ animation: idea-spin 1s linear infinite;
362
+ }
363
+
364
+ /* Error indicator */
365
+ .flowdrop-idea-node__error {
366
+ display: flex;
367
+ align-items: center;
368
+ gap: 0.5rem;
369
+ padding: 0.5rem 1rem;
370
+ font-size: 0.75rem;
371
+ color: #ef4444;
372
+ border-top: 1px solid #fecaca;
373
+ background-color: #fef2f2;
374
+ }
375
+
376
+ :global(.flowdrop-idea-node__error-icon) {
377
+ width: 0.875rem;
378
+ height: 0.875rem;
379
+ }
380
+
381
+ @keyframes idea-spin {
382
+ to {
383
+ transform: rotate(360deg);
384
+ }
385
+ }
386
+
387
+ /* Config button */
388
+ .flowdrop-idea-node__config-btn {
389
+ position: absolute;
390
+ top: 0.625rem;
391
+ right: 0.625rem;
392
+ width: 1.5rem;
393
+ height: 1.5rem;
394
+ background-color: rgba(255, 255, 255, 0.95);
395
+ border: 1px solid #e5e7eb;
396
+ border-radius: 0.375rem;
397
+ color: #6b7280;
398
+ cursor: pointer;
399
+ display: flex;
400
+ align-items: center;
401
+ justify-content: center;
402
+ opacity: 0;
403
+ transition: all 0.2s ease-in-out;
404
+ backdrop-filter: blur(4px);
405
+ z-index: 15;
406
+ font-size: 0.875rem;
407
+ }
408
+
409
+ .flowdrop-idea-node:hover .flowdrop-idea-node__config-btn {
410
+ opacity: 1;
411
+ }
412
+
413
+ .flowdrop-idea-node__config-btn:hover {
414
+ background-color: #f9fafb;
415
+ border-color: #d1d5db;
416
+ color: #374151;
417
+ transform: scale(1.05);
418
+ }
419
+
420
+ /* Handle styles */
421
+ :global(.flowdrop-idea-node .svelte-flow__handle) {
422
+ width: 16px !important;
423
+ height: 16px !important;
424
+ border-radius: 50% !important;
425
+ border: 2px solid #ffffff !important;
426
+ transition: all 0.2s ease-in-out !important;
427
+ cursor: pointer !important;
428
+ z-index: 20 !important;
429
+ pointer-events: auto !important;
430
+ }
431
+
432
+ /* Left handle positioning */
433
+ :global(.flowdrop-idea-node .svelte-flow__handle-left) {
434
+ left: -8px !important;
435
+ }
436
+
437
+ /* Right handle positioning */
438
+ :global(.flowdrop-idea-node .svelte-flow__handle-right) {
439
+ right: -8px !important;
440
+ }
441
+
442
+ /* Top handle positioning */
443
+ :global(.flowdrop-idea-node .svelte-flow__handle-top) {
444
+ top: -8px !important;
445
+ }
446
+
447
+ /* Bottom handle positioning */
448
+ :global(.flowdrop-idea-node .svelte-flow__handle-bottom) {
449
+ bottom: -8px !important;
450
+ }
451
+
452
+ /* Handle hover effects */
453
+ :global(.flowdrop-idea-node .svelte-flow__handle-left:hover),
454
+ :global(.flowdrop-idea-node .svelte-flow__handle-right:hover) {
455
+ transform: translateY(-50%) scale(1.2) !important;
456
+ }
457
+
458
+ :global(.flowdrop-idea-node .svelte-flow__handle-top:hover),
459
+ :global(.flowdrop-idea-node .svelte-flow__handle-bottom:hover) {
460
+ transform: translateX(-50%) scale(1.2) !important;
461
+ }
462
+
463
+ :global(.flowdrop-idea-node .svelte-flow__handle:focus) {
464
+ outline: 2px solid #3b82f6 !important;
465
+ outline-offset: 2px !important;
466
+ }
467
+
468
+ /* Responsive design */
469
+ @media (max-width: 640px) {
470
+ .flowdrop-idea-node {
471
+ width: 16rem;
472
+ }
473
+
474
+ .flowdrop-idea-node__header {
475
+ padding: 0.625rem 0.75rem 0.375rem;
476
+ }
477
+
478
+ .flowdrop-idea-node__body {
479
+ padding: 0 0.75rem 0.625rem;
480
+ }
481
+
482
+ .flowdrop-idea-node__title {
483
+ font-size: 0.875rem;
484
+ }
485
+
486
+ .flowdrop-idea-node__description {
487
+ font-size: 0.75rem;
488
+ }
489
+ }
490
+ </style>
@@ -0,0 +1,24 @@
1
+ import type { ConfigValues, NodeMetadata } from "../../types/index.js";
2
+ type $$ComponentProps = {
3
+ data: {
4
+ label: string;
5
+ config: ConfigValues;
6
+ metadata: NodeMetadata;
7
+ nodeId?: string;
8
+ onConfigOpen?: (node: {
9
+ id: string;
10
+ type: string;
11
+ data: {
12
+ label: string;
13
+ config: ConfigValues;
14
+ metadata: NodeMetadata;
15
+ };
16
+ }) => void;
17
+ };
18
+ selected?: boolean;
19
+ isProcessing?: boolean;
20
+ isError?: boolean;
21
+ };
22
+ declare const IdeaNode: import("svelte").Component<$$ComponentProps, {}, "">;
23
+ type IdeaNode = ReturnType<typeof IdeaNode>;
24
+ export default IdeaNode;
@@ -52,6 +52,26 @@
52
52
  (props.data.metadata?.color as string) || (props.data.config?.color as string) || '#6366f1'
53
53
  );
54
54
 
55
+ /**
56
+ * Instance-specific title override from config.
57
+ * Falls back to the original label if not set.
58
+ * This allows users to customize the node title per-instance via config.
59
+ */
60
+ const displayTitle = $derived(
61
+ (props.data.config?.instanceTitle as string) || props.data.label
62
+ );
63
+
64
+ /**
65
+ * Instance-specific description override from config.
66
+ * Falls back to the metadata description if not set.
67
+ * This allows users to customize the node description per-instance via config.
68
+ */
69
+ const displayDescription = $derived(
70
+ (props.data.config?.instanceDescription as string) ||
71
+ props.data.metadata?.description ||
72
+ 'A configurable simple node'
73
+ );
74
+
55
75
  // Handle configuration sidebar - now using global ConfigSidebar
56
76
  function openConfigSidebar(): void {
57
77
  if (props.data.onConfigOpen) {
@@ -198,13 +218,13 @@
198
218
 
199
219
  <!-- Node Title -->
200
220
  <h3 class="flowdrop-simple-node__title">
201
- {props.data.label}
221
+ {displayTitle}
202
222
  </h3>
203
223
  </div>
204
224
 
205
225
  <!-- Node Description -->
206
226
  <p class="flowdrop-simple-node__description">
207
- {props.data.metadata?.description || 'A configurable simple node'}
227
+ {displayDescription}
208
228
  </p>
209
229
  </div>
210
230
 
@@ -150,9 +150,27 @@
150
150
  );
151
151
 
152
152
  /**
153
- * Get display label
153
+ * Instance-specific title override from config.
154
+ * Falls back to the original label if not set.
155
+ * This allows users to customize the node title per-instance via config.
154
156
  */
155
- let displayLabel = $derived(props.data.label || props.data.metadata?.name || variantConfig.label);
157
+ const displayTitle = $derived(
158
+ (props.data.config?.instanceTitle as string) ||
159
+ props.data.label ||
160
+ props.data.metadata?.name ||
161
+ variantConfig.label
162
+ );
163
+
164
+ /**
165
+ * Instance-specific description override from config.
166
+ * Falls back to the metadata description if not set.
167
+ * This allows users to customize the node description per-instance via config.
168
+ */
169
+ const displayDescription = $derived(
170
+ (props.data.config?.instanceDescription as string) ||
171
+ props.data.metadata?.description ||
172
+ ""
173
+ );
156
174
 
157
175
  /**
158
176
  * Check if metadata explicitly defines inputs (including empty array)
@@ -282,7 +300,7 @@
282
300
  onkeydown={handleKeydown}
283
301
  role="button"
284
302
  tabindex="0"
285
- aria-label="{variant} node: {displayLabel}"
303
+ aria-label="{variant} node: {displayTitle}"
286
304
  >
287
305
  <!-- Config button at top -->
288
306
  <button
@@ -333,9 +351,16 @@
333
351
  {/if}
334
352
  </div>
335
353
 
336
- <!-- Label below the circle -->
337
- <div class="flowdrop-terminal-node__label">
338
- {displayLabel}
354
+ <!-- Label and description below the circle -->
355
+ <div class="flowdrop-terminal-node__label-container">
356
+ <div class="flowdrop-terminal-node__label">
357
+ {displayTitle}
358
+ </div>
359
+ {#if displayDescription}
360
+ <div class="flowdrop-terminal-node__description">
361
+ {displayDescription}
362
+ </div>
363
+ {/if}
339
364
  </div>
340
365
 
341
366
  <!-- Processing indicator -->
@@ -440,19 +465,37 @@
440
465
  filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.1));
441
466
  }
442
467
 
468
+ .flowdrop-terminal-node__label-container {
469
+ display: flex;
470
+ flex-direction: column;
471
+ align-items: center;
472
+ gap: 0.125rem;
473
+ background-color: rgba(255, 255, 255, 0.9);
474
+ padding: 0.25rem 0.5rem;
475
+ border-radius: 0.25rem;
476
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
477
+ max-width: 140px;
478
+ }
479
+
443
480
  .flowdrop-terminal-node__label {
444
481
  font-size: 0.75rem;
445
482
  font-weight: 500;
446
483
  color: #374151;
447
484
  text-align: center;
448
- max-width: 100px;
449
485
  overflow: hidden;
450
486
  text-overflow: ellipsis;
451
487
  white-space: nowrap;
452
- background-color: rgba(255, 255, 255, 0.9);
453
- padding: 0.125rem 0.5rem;
454
- border-radius: 0.25rem;
455
- box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
488
+ max-width: 100%;
489
+ }
490
+
491
+ .flowdrop-terminal-node__description {
492
+ font-size: 0.625rem;
493
+ color: #6b7280;
494
+ text-align: center;
495
+ overflow: hidden;
496
+ text-overflow: ellipsis;
497
+ white-space: nowrap;
498
+ max-width: 100%;
456
499
  }
457
500
 
458
501
  .flowdrop-terminal-node__processing {
@@ -47,17 +47,32 @@
47
47
  let toolColor = $derived(
48
48
  (props.data.metadata?.color as string) || (props.data.config?.color as string) || '#f59e0b'
49
49
  );
50
- let toolName = $derived(
51
- (props.data.metadata?.name as string) ||
50
+
51
+ /**
52
+ * Instance-specific title override from config.
53
+ * Falls back to metadata name, toolName config, or label if not set.
54
+ * This allows users to customize the tool title per-instance via config.
55
+ */
56
+ const displayTitle = $derived(
57
+ (props.data.config?.instanceTitle as string) ||
58
+ (props.data.metadata?.name as string) ||
52
59
  (props.data.config?.toolName as string) ||
53
60
  props.data.label ||
54
- 'Tool'
61
+ "Tool"
55
62
  );
56
- let toolDescription = $derived(
57
- (props.data.metadata?.description as string) ||
63
+
64
+ /**
65
+ * Instance-specific description override from config.
66
+ * Falls back to metadata description or toolDescription config if not set.
67
+ * This allows users to customize the tool description per-instance via config.
68
+ */
69
+ const displayDescription = $derived(
70
+ (props.data.config?.instanceDescription as string) ||
71
+ (props.data.metadata?.description as string) ||
58
72
  (props.data.config?.toolDescription as string) ||
59
- 'A configurable tool for agents'
73
+ "A configurable tool for agents"
60
74
  );
75
+
61
76
  let toolVersion = $derived(
62
77
  (props.data.metadata?.version as string) ||
63
78
  (props.data.config?.toolVersion as string) ||
@@ -159,7 +174,7 @@
159
174
  <!-- Tool Info -->
160
175
  <div class="flowdrop-tool-node__info">
161
176
  <h3 class="flowdrop-tool-node__title">
162
- {toolName}
177
+ {displayTitle}
163
178
  </h3>
164
179
  <div class="flowdrop-tool-node__version">
165
180
  v{toolVersion}
@@ -170,9 +185,9 @@
170
185
  <div class="flowdrop-tool-node__badge">TOOL</div>
171
186
  </div>
172
187
 
173
- <!-- Tool Description -->
188
+ <!-- Tool Description - uses instanceDescription override if set -->
174
189
  <p class="flowdrop-tool-node__description">
175
- {toolDescription}
190
+ {displayDescription}
176
191
  </p>
177
192
  </div>
178
193
 
@@ -28,6 +28,24 @@
28
28
  let props: Props = $props();
29
29
  let isHandleInteraction = $state(false);
30
30
 
31
+ /**
32
+ * Instance-specific title override from config.
33
+ * Falls back to the original label if not set.
34
+ * This allows users to customize the node title per-instance via config.
35
+ */
36
+ const displayTitle = $derived(
37
+ (props.data.config?.instanceTitle as string) || props.data.label
38
+ );
39
+
40
+ /**
41
+ * Instance-specific description override from config.
42
+ * Falls back to the metadata description if not set.
43
+ * This allows users to customize the node description per-instance via config.
44
+ */
45
+ const displayDescription = $derived(
46
+ (props.data.config?.instanceDescription as string) || props.data.metadata.description
47
+ );
48
+
31
49
  /**
32
50
  * Get the hideUnconnectedHandles setting from extensions
33
51
  * Merges node type defaults with instance overrides
@@ -173,7 +191,7 @@
173
191
 
174
192
  <!-- Node Title - Icon and Title on same line -->
175
193
  <h3 class="flowdrop-text--sm flowdrop-font--medium flowdrop-truncate flowdrop-flex--1">
176
- {props.data.label}
194
+ {displayTitle}
177
195
  </h3>
178
196
 
179
197
  <!-- Status Indicators -->
@@ -186,7 +204,7 @@
186
204
  class="flowdrop-text--xs flowdrop-text--gray flowdrop-truncate flowdrop-mt--1"
187
205
  id="node-description-{props.data.nodeId || 'unknown'}"
188
206
  >
189
- {props.data.metadata.description}
207
+ {displayDescription}
190
208
  </p>
191
209
  </div>
192
210
 
@@ -70,7 +70,7 @@ export declare function getBuiltinTypes(): string[];
70
70
  * Type for built-in node types.
71
71
  * Use this when you specifically need a built-in type.
72
72
  */
73
- export type BuiltinNodeType = 'workflowNode' | 'simple' | 'square' | 'tool' | 'gateway' | 'note' | 'terminal';
73
+ export type BuiltinNodeType = 'workflowNode' | 'simple' | 'square' | 'tool' | 'gateway' | 'note' | 'terminal' | 'idea';
74
74
  /**
75
75
  * Array of built-in type strings for runtime validation.
76
76
  */
@@ -13,6 +13,7 @@ import ToolNode from '../components/nodes/ToolNode.svelte';
13
13
  import GatewayNode from '../components/nodes/GatewayNode.svelte';
14
14
  import NotesNode from '../components/nodes/NotesNode.svelte';
15
15
  import TerminalNode from '../components/nodes/TerminalNode.svelte';
16
+ import IdeaNode from '../components/nodes/IdeaNode.svelte';
16
17
  /**
17
18
  * Source identifier for built-in FlowDrop components
18
19
  */
@@ -98,6 +99,17 @@ export const BUILTIN_NODE_COMPONENTS = [
98
99
  source: FLOWDROP_SOURCE,
99
100
  statusPosition: 'top-right',
100
101
  statusSize: 'sm'
102
+ },
103
+ {
104
+ type: 'idea',
105
+ displayName: 'Idea (Conceptual Flow)',
106
+ description: 'Conceptual idea node for BPMN-like flow diagrams',
107
+ component: IdeaNode,
108
+ icon: 'mdi:lightbulb-outline',
109
+ category: 'layout',
110
+ source: FLOWDROP_SOURCE,
111
+ statusPosition: 'top-right',
112
+ statusSize: 'sm'
101
113
  }
102
114
  ];
103
115
  /**
@@ -188,7 +200,8 @@ export const BUILTIN_NODE_TYPES = [
188
200
  'tool',
189
201
  'gateway',
190
202
  'note',
191
- 'terminal'
203
+ 'terminal',
204
+ 'idea'
192
205
  ];
193
206
  // Auto-register built-ins when this module is imported
194
207
  registerBuiltinNodes();
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@d34dman/flowdrop",
3
3
  "license": "MIT",
4
4
  "private": false,
5
- "version": "0.0.35",
5
+ "version": "0.0.37",
6
6
  "scripts": {
7
7
  "dev": "vite dev",
8
8
  "build": "vite build && npm run prepack",