@flowdrop/flowdrop 2.0.0-beta.2 → 2.0.0-beta.3

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 (85) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/MIGRATION-2.0.md +13 -0
  3. package/dist/components/App.svelte +21 -45
  4. package/dist/components/App.svelte.d.ts +2 -7
  5. package/dist/components/CanvasIconButton.svelte +76 -0
  6. package/dist/components/CanvasIconButton.svelte.d.ts +18 -0
  7. package/dist/components/ConfigForm.svelte +0 -19
  8. package/dist/components/ConfigPanel.svelte +4 -3
  9. package/dist/components/LogoWordmark.svelte +113 -0
  10. package/dist/components/LogoWordmark.svelte.d.ts +26 -0
  11. package/dist/components/Navbar.svelte +8 -59
  12. package/dist/components/NodeSidebar.svelte +4 -11
  13. package/dist/components/NodeSwapPicker.svelte +0 -2
  14. package/dist/components/PortMappingRow.svelte +0 -2
  15. package/dist/components/SchemaForm.svelte +0 -12
  16. package/dist/components/SettingsModal.svelte +0 -5
  17. package/dist/components/SettingsPanel.svelte +2 -6
  18. package/dist/components/ThemeToggle.svelte +0 -5
  19. package/dist/components/UniversalNode.svelte +32 -1
  20. package/dist/components/WorkflowEditor.svelte +62 -51
  21. package/dist/components/WorkflowEditor.svelte.d.ts +18 -0
  22. package/dist/components/chat/AIChatPanel.svelte +1 -1
  23. package/dist/components/console/ConsoleAutocomplete.svelte +1 -1
  24. package/dist/components/console/ConsoleOutput.svelte +2 -2
  25. package/dist/components/form/FormArray.svelte +0 -16
  26. package/dist/components/form/FormAutocomplete.svelte +10 -6
  27. package/dist/components/form/FormCheckboxGroup.svelte +0 -4
  28. package/dist/components/form/FormCodeEditor.svelte +9 -7
  29. package/dist/components/form/FormFieldLight.svelte +3 -3
  30. package/dist/components/form/FormMarkdownEditor.svelte +8 -5
  31. package/dist/components/form/FormNumberField.svelte +0 -4
  32. package/dist/components/form/FormRangeField.svelte +1 -20
  33. package/dist/components/form/FormSelect.svelte +10 -6
  34. package/dist/components/form/FormTemplateEditor.svelte +6 -4
  35. package/dist/components/form/FormTextField.svelte +10 -6
  36. package/dist/components/form/FormTextarea.svelte +10 -6
  37. package/dist/components/form/FormToggle.svelte +0 -4
  38. package/dist/components/icons/CommandLineIcon.svelte +15 -0
  39. package/dist/components/icons/CommandLineIcon.svelte.d.ts +26 -0
  40. package/dist/components/icons/MenuIcon.svelte +4 -0
  41. package/dist/components/icons/MenuIcon.svelte.d.ts +26 -0
  42. package/dist/components/icons/MenuOpenIcon.svelte +6 -0
  43. package/dist/components/icons/MenuOpenIcon.svelte.d.ts +26 -0
  44. package/dist/components/interrupt/ChoicePrompt.svelte +0 -10
  45. package/dist/components/interrupt/ConfirmationPrompt.svelte +0 -5
  46. package/dist/components/interrupt/InterruptBubble.svelte +0 -10
  47. package/dist/components/interrupt/ReviewPrompt.svelte +0 -20
  48. package/dist/components/interrupt/TextInputPrompt.svelte +0 -6
  49. package/dist/components/layouts/MainLayout.svelte +4 -5
  50. package/dist/components/nodes/AtomNode.svelte +46 -34
  51. package/dist/components/nodes/GatewayNode.svelte +91 -99
  52. package/dist/components/nodes/IdeaNode.svelte +62 -90
  53. package/dist/components/nodes/NodeConfigButton.svelte +86 -0
  54. package/dist/components/nodes/NodeConfigButton.svelte.d.ts +15 -0
  55. package/dist/components/nodes/NotesNode.svelte +70 -81
  56. package/dist/components/nodes/SimpleNode.svelte +28 -78
  57. package/dist/components/nodes/SquareNode.svelte +79 -109
  58. package/dist/components/nodes/TerminalNode.svelte +28 -86
  59. package/dist/components/nodes/ToolNode.svelte +82 -95
  60. package/dist/components/nodes/WorkflowNode.svelte +91 -100
  61. package/dist/components/playground/ChatInput.svelte +0 -1
  62. package/dist/components/playground/InputCollector.svelte +0 -2
  63. package/dist/components/playground/PlaygroundApp.svelte +1 -1
  64. package/dist/components/playground/PlaygroundStudio.svelte +0 -5
  65. package/dist/playground/mount.d.ts +9 -5
  66. package/dist/playground/mount.js +9 -5
  67. package/dist/skins/drafter.d.ts +30 -0
  68. package/dist/skins/drafter.js +185 -0
  69. package/dist/skins/index.d.ts +2 -1
  70. package/dist/skins/index.js +4 -2
  71. package/dist/styles/base.css +38 -9
  72. package/dist/styles/tokens.css +54 -2
  73. package/dist/svelte-app.d.ts +6 -0
  74. package/dist/svelte-app.js +3 -2
  75. package/dist/themes/drafter.d.ts +2 -0
  76. package/dist/themes/drafter.js +15 -0
  77. package/dist/themes/index.d.ts +2 -1
  78. package/dist/themes/index.js +8 -2
  79. package/dist/types/events.d.ts +18 -0
  80. package/dist/types/events.js +2 -1
  81. package/dist/types/settings.d.ts +1 -1
  82. package/dist/types/settings.js +1 -1
  83. package/dist/types/skin.d.ts +1 -1
  84. package/dist/types/theme.d.ts +16 -2
  85. package/package.json +1 -1
@@ -10,7 +10,7 @@
10
10
  import { getDataTypeColor, getCategoryColorToken } from '../../utils/colors';
11
11
  import { getInstance } from '../../stores/getInstance.svelte.js';
12
12
  import type { NodeMetadata, NodePort } from '../../types/index.js';
13
- import CogIcon from '../icons/CogIcon.svelte';
13
+ import NodeConfigButton from './NodeConfigButton.svelte';
14
14
  import AlertCircleIcon from '../icons/AlertCircleIcon.svelte';
15
15
 
16
16
  interface ToolNodeParameter {
@@ -128,6 +128,13 @@
128
128
  props.data.metadata?.outputs?.find((port: NodePort) => port.dataType === portDataType)
129
129
  );
130
130
 
131
+ /**
132
+ * Vertical center of the first port handle (px). Single source of truth shared
133
+ * by the input/output handles and the badge overlay, so the badge stays level
134
+ * with the first port.
135
+ */
136
+ const firstPortTop = 40;
137
+
131
138
  /**
132
139
  * Handle configuration sidebar - using global ConfigSidebar
133
140
  */
@@ -149,23 +156,6 @@
149
156
  function handleDoubleClick(): void {
150
157
  openConfigSidebar();
151
158
  }
152
-
153
- /**
154
- * Handle click events
155
- */
156
- function handleClick(): void {
157
- // Node selection is handled by Svelte Flow
158
- }
159
-
160
- /**
161
- * Handle keyboard events for accessibility
162
- */
163
- function handleKeydown(event: KeyboardEvent): void {
164
- if (event.key === 'Enter' || event.key === ' ') {
165
- event.preventDefault();
166
- handleDoubleClick();
167
- }
168
- }
169
159
  </script>
170
160
 
171
161
  <!-- Tool Input Handle (optional): center at 40px (multiple of 10), 20px connection area -->
@@ -174,7 +164,7 @@
174
164
  type="target"
175
165
  position={Position.Left}
176
166
  id={`${props.data.nodeId}-input-${toolInputPort.id}`}
177
- style="top: 40px; transform: translateY(-50%); z-index: 30; --fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColor(
167
+ style="top: {firstPortTop}px; transform: translateY(-50%); z-index: 30; --fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColor(
178
168
  checker,
179
169
  portDataType
180
170
  )}); --fd-handle-border-color: var(--fd-handle-border);"
@@ -182,17 +172,16 @@
182
172
  {/if}
183
173
 
184
174
  <!-- Tool Node -->
175
+ <!-- Presentational: focus, keyboard and selection live on xyflow's node
176
+ wrapper (see UniversalNode). double-click is a mouse convenience. -->
177
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
185
178
  <div
186
179
  class="flowdrop-tool-node"
187
180
  class:flowdrop-tool-node--selected={props.selected}
188
181
  class:flowdrop-tool-node--processing={props.isProcessing}
189
182
  class:flowdrop-tool-node--error={props.isError}
190
183
  style={nodeStyle}
191
- onclick={handleClick}
192
184
  ondblclick={handleDoubleClick}
193
- onkeydown={handleKeydown}
194
- role="button"
195
- tabindex="0"
196
185
  >
197
186
  <!-- Node Header -->
198
187
  <div class="flowdrop-tool-node__header">
@@ -217,8 +206,8 @@
217
206
  </div>
218
207
  </div>
219
208
 
220
- <!-- Tool Badge - tinted style matching icon wrappers -->
221
- <div class="flowdrop-tool-node__badge">{displayBadge}</div>
209
+ <!-- Tool Badge - overlay aligned to the first port's vertical center -->
210
+ <div class="flowdrop-tool-node__badge" style="top: {firstPortTop}px">{displayBadge}</div>
222
211
  </div>
223
212
 
224
213
  <!-- Tool Description - uses instanceDescription override if set -->
@@ -242,9 +231,7 @@
242
231
  {/if}
243
232
 
244
233
  <!-- Config button -->
245
- <button class="flowdrop-tool-node__config-btn" onclick={openConfigSidebar} title="Configure tool">
246
- <CogIcon />
247
- </button>
234
+ <NodeConfigButton onclick={openConfigSidebar} title="Configure tool" />
248
235
  </div>
249
236
 
250
237
  <!-- Tool Output Handle (optional): center at 40px (multiple of 10), 20px connection area -->
@@ -253,7 +240,7 @@
253
240
  type="source"
254
241
  position={Position.Right}
255
242
  id={`${props.data.nodeId}-output-${toolOutputPort.id}`}
256
- style="top: 40px; transform: translateY(-50%); z-index: 30; --fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColor(
243
+ style="top: {firstPortTop}px; transform: translateY(-50%); z-index: 30; --fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColor(
257
244
  checker,
258
245
  portDataType
259
246
  )}); --fd-handle-border-color: var(--fd-handle-border);"
@@ -263,44 +250,46 @@
263
250
  <style>
264
251
  .flowdrop-tool-node {
265
252
  position: relative;
266
- background-color: var(--fd-card);
267
- border: 1.5px solid var(--fd-tool-node-color);
268
- border-radius: var(--fd-radius-xl);
253
+ box-sizing: border-box;
254
+ background-color: var(--fd-node-bg);
255
+ backdrop-filter: var(--fd-node-backdrop-filter);
256
+ border: var(--fd-node-border-width) solid var(--fd-tool-node-color);
257
+ border-radius: var(--fd-node-radius);
269
258
  width: var(--fd-node-default-width);
270
- min-height: var(--fd-node-tool-min-height);
259
+ /* A tool has at most 2 ports on a side (handles at 40px & 80px), so 100px
260
+ is the fixed floor; the grid-aligned header keeps it a 20px multiple. */
261
+ min-height: 100px;
271
262
  display: flex;
272
263
  flex-direction: column;
273
264
  cursor: pointer;
274
265
  transition: all var(--fd-transition-fast);
275
- box-shadow: var(--fd-shadow-md);
266
+ box-shadow: var(--fd-node-shadow);
276
267
  overflow: visible;
277
268
  z-index: 10;
278
269
  color: var(--fd-foreground);
279
270
  }
280
271
 
281
272
  .flowdrop-tool-node:hover {
282
- box-shadow: var(--fd-shadow-lg);
273
+ box-shadow: var(--fd-node-shadow-hover);
283
274
  border-color: var(--fd-tool-node-color);
284
275
  }
285
276
 
286
277
  .flowdrop-tool-node--selected {
287
278
  box-shadow:
288
279
  0 0 0 2px color-mix(in srgb, var(--fd-tool-node-color) 30%, transparent),
289
- var(--fd-shadow-lg);
280
+ var(--fd-node-shadow-hover);
290
281
  border-color: var(--fd-tool-node-color);
291
282
  }
292
283
 
293
284
  .flowdrop-tool-node--selected:hover {
294
285
  box-shadow:
295
286
  0 0 0 2px color-mix(in srgb, var(--fd-tool-node-color) 30%, transparent),
296
- var(--fd-shadow-lg);
287
+ var(--fd-node-shadow-hover);
297
288
  border-color: var(--fd-tool-node-color);
298
289
  }
299
290
 
300
- .flowdrop-tool-node:focus-visible {
301
- outline: 2px solid var(--fd-ring);
302
- outline-offset: 2px;
303
- }
291
+ /* Focus ring is centralized in base.css (drawn on the .svelte-flow__node
292
+ wrapper, which is the focusable element). */
304
293
 
305
294
  .flowdrop-tool-node--processing {
306
295
  opacity: 0.7;
@@ -312,10 +301,18 @@
312
301
  }
313
302
 
314
303
  .flowdrop-tool-node__header {
315
- padding: 1rem;
304
+ box-sizing: border-box;
305
+ flex: 1;
306
+ display: flex;
307
+ flex-direction: column;
308
+ /* px on the 20px grid. Bottom padding absorbs BOTH node borders (top + bottom)
309
+ so the OUTER node height lands on a 20px multiple: with a 40px title row and
310
+ a 20px-per-line description, outer = 60 + 20·lines (80, 100, 120…). */
311
+ padding: var(--fd-node-header-gap) 20px
312
+ calc(var(--fd-node-header-gap) - var(--fd-node-border-width) * 2);
316
313
  /* Light mode: mix tool color with white (95%) for subtle tint */
317
314
  background-color: color-mix(in srgb, var(--fd-tool-node-color) 5%, white);
318
- border-radius: var(--fd-radius-xl);
315
+ border-radius: var(--fd-node-radius);
319
316
  border: none;
320
317
  }
321
318
 
@@ -329,8 +326,10 @@
329
326
  .flowdrop-tool-node__header-content {
330
327
  display: flex;
331
328
  align-items: center;
332
- gap: 0.75rem;
333
- margin-bottom: 0.5rem;
329
+ gap: 12px;
330
+ /* Two grid rows (title + version), so the icon + text block is exactly 40px;
331
+ the description stacks flush below and each wrapped line adds one 20px row. */
332
+ min-height: var(--fd-node-header-title-height);
334
333
  }
335
334
 
336
335
  /* Squircle icon wrapper - Apple-style rounded square background */
@@ -338,9 +337,10 @@
338
337
  display: var(--fd-node-icon-display, flex);
339
338
  align-items: center;
340
339
  justify-content: center;
341
- width: 2.5rem;
342
- height: 2.5rem;
343
- border-radius: 0.625rem;
340
+ /* px (not rem) so the icon stays grid-locked regardless of root font-size */
341
+ width: 36px;
342
+ height: 36px;
343
+ border-radius: 8px;
344
344
  background: color-mix(
345
345
  in srgb,
346
346
  var(--fd-tool-node-color) var(--fd-node-icon-bg-opacity),
@@ -365,41 +365,64 @@
365
365
  }
366
366
 
367
367
  .flowdrop-tool-node__title {
368
- font-size: 1rem;
368
+ font-size: 16px;
369
369
  font-weight: 600;
370
370
  color: var(--fd-foreground);
371
371
  margin: 0;
372
- line-height: 1.4;
372
+ /* one 20px grid row */
373
+ line-height: var(--fd-node-port-row-height);
374
+ overflow: hidden;
375
+ text-overflow: ellipsis;
376
+ white-space: nowrap;
373
377
  }
374
378
 
375
379
  .flowdrop-tool-node__version {
376
380
  font-size: var(--fd-text-xs);
377
381
  color: var(--fd-muted-foreground);
378
382
  font-weight: 500;
379
- margin-top: 0.125rem;
383
+ /* one 20px grid row */
384
+ line-height: var(--fd-node-port-row-height);
380
385
  }
381
386
 
382
387
  .flowdrop-tool-node__badge {
388
+ /* Overlay so it takes no row space — the title gets the full width. The
389
+ inline `top` is set to the first port's center; translateY keeps the
390
+ badge vertically aligned with that port. */
391
+ position: absolute;
392
+ right: 20px;
393
+ transform: translateY(-50%);
394
+ z-index: 12;
383
395
  background-color: color-mix(in srgb, var(--fd-tool-node-color) 15%, transparent);
384
396
  color: var(--fd-tool-node-color);
385
397
  border: 1px solid color-mix(in srgb, var(--fd-tool-node-color) 30%, transparent);
386
- font-size: 0.625rem;
398
+ font-size: 10px;
387
399
  font-weight: 700;
388
- padding: 0.25rem 0.5rem;
400
+ padding: 4px 8px;
389
401
  border-radius: var(--fd-radius-sm);
390
402
  letter-spacing: 0.05em;
403
+ /* Lay flat: sit back at reduced opacity so it reads as a subtle marker and
404
+ any title underneath stays legible. */
405
+ opacity: 0.4;
391
406
  }
392
407
 
393
408
  .flowdrop-tool-node__description {
394
409
  font-size: var(--fd-text-xs);
395
410
  color: var(--fd-muted-foreground);
396
411
  margin: 0;
397
- line-height: 1.3;
412
+ /* each line is one 20px grid row; clamp so the node grows in clean 20px steps */
413
+ line-height: var(--fd-node-port-row-height);
414
+ min-height: var(--fd-node-port-row-height);
415
+ overflow: hidden;
416
+ text-overflow: ellipsis;
417
+ display: -webkit-box;
418
+ -webkit-line-clamp: 2;
419
+ line-clamp: 2;
420
+ -webkit-box-orient: vertical;
398
421
  }
399
422
 
400
423
  .flowdrop-tool-node__icon-wrapper :global(.flowdrop-tool-node__icon) {
401
- width: 1.5rem;
402
- height: 1.5rem;
424
+ width: 20px;
425
+ height: 20px;
403
426
  color: var(--fd-node-icon);
404
427
  }
405
428
 
@@ -430,40 +453,9 @@
430
453
  height: 12px;
431
454
  }
432
455
 
433
- .flowdrop-tool-node__config-btn :global(svg) {
434
- width: 14px;
435
- height: 14px;
436
- }
437
-
438
- .flowdrop-tool-node__config-btn {
439
- position: absolute;
440
- top: 0.5rem;
441
- right: 0.5rem;
442
- width: 1.5rem;
443
- height: 1.5rem;
444
- background-color: var(--fd-backdrop);
445
- border: 1px solid var(--fd-border);
446
- border-radius: var(--fd-radius-sm);
447
- color: var(--fd-muted-foreground);
448
- cursor: pointer;
449
- display: flex;
450
- align-items: center;
451
- justify-content: center;
452
- opacity: 0;
453
- transition: all var(--fd-transition-normal);
454
- backdrop-filter: blur(4px);
455
- z-index: 15;
456
- font-size: var(--fd-text-sm);
457
- }
458
-
459
- .flowdrop-tool-node:hover .flowdrop-tool-node__config-btn {
460
- opacity: 1;
461
- }
462
-
463
- .flowdrop-tool-node__config-btn:hover {
464
- background-color: var(--fd-muted);
465
- border-color: var(--fd-border-strong);
466
- color: var(--fd-foreground);
456
+ /* Reveal the NodeConfigButton (gear) when the node is hovered. */
457
+ .flowdrop-tool-node:hover {
458
+ --fd-config-btn-opacity: 1;
467
459
  }
468
460
 
469
461
  @keyframes spin {
@@ -486,11 +478,6 @@
486
478
  box-shadow: 0 0 0 2px color-mix(in srgb, var(--fd-tool-node-color) 30%, transparent) !important;
487
479
  }
488
480
 
489
- :global(.svelte-flow__node-tool .svelte-flow__handle:focus) {
490
- outline: 2px solid var(--fd-tool-node-color) !important;
491
- outline-offset: 2px !important;
492
- }
493
-
494
481
  /* Circle dot icon — shown in minimal skin via --fd-node-circle-display */
495
482
  .flowdrop-tool-node__color-dot {
496
483
  width: 10px;
@@ -16,7 +16,7 @@
16
16
  import { dynamicPortToNodePort } from '../../types/index.js';
17
17
  import Icon from '@iconify/svelte';
18
18
  import { getNodeIcon } from '../../utils/icons.js';
19
- import CogIcon from '../icons/CogIcon.svelte';
19
+ import NodeConfigButton from './NodeConfigButton.svelte';
20
20
  import {
21
21
  getDataTypeColorToken,
22
22
  getCategoryColorToken,
@@ -165,13 +165,6 @@
165
165
  allOutputPorts.filter((port) => isPortVisible(port, 'output'))
166
166
  );
167
167
 
168
- /**
169
- * Handle node click - only handle selection, no config opening
170
- */
171
- function handleNodeClick(): void {
172
- // Node selection is handled by Svelte Flow
173
- }
174
-
175
168
  /**
176
169
  * Handle double-click to open config
177
170
  */
@@ -196,23 +189,17 @@
196
189
  </script>
197
190
 
198
191
  <!-- Node Container -->
192
+ <!-- Presentational: focus, keyboard and selection live on xyflow's node
193
+ wrapper (see UniversalNode). double-click is a mouse convenience. -->
194
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
199
195
  <div
200
196
  class="flowdrop-workflow-node"
201
197
  class:flowdrop-workflow-node--selected={props.selected}
202
- onclick={handleNodeClick}
203
198
  ondblclick={handleDoubleClick}
204
199
  onmouseup={() => {
205
200
  isHandleInteraction = false;
206
201
  }}
207
202
  data-handle-interaction={isHandleInteraction}
208
- role="button"
209
- tabindex="0"
210
- onkeydown={(e) => {
211
- if (e.key === 'Enter' || e.key === ' ') {
212
- e.preventDefault();
213
- handleDoubleClick();
214
- }
215
- }}
216
203
  aria-label={graph.workflowNode({ name: props.data.metadata.name })}
217
204
  aria-describedby="node-description-{props.data.nodeId || 'unknown'}"
218
205
  >
@@ -236,7 +223,7 @@
236
223
  ></span>
237
224
 
238
225
  <!-- Node Title - Icon and Title on same line -->
239
- <h3 class="flowdrop-text--sm flowdrop-font--medium flowdrop-truncate flowdrop-flex--1">
226
+ <h3 class="flowdrop-text--sm flowdrop-font--medium flowdrop-flex--1">
240
227
  {displayTitle}
241
228
  </h3>
242
229
 
@@ -258,13 +245,13 @@
258
245
  <div class="flowdrop-workflow-node__ports-list">
259
246
  {#each visibleInputPorts as port (port.id)}
260
247
  <div class="flowdrop-workflow-node__port">
261
- <!-- Input Handle: centered in row, at node edge (ports have no padding) -->
248
+ <!-- Input Handle: one grid row (20px) from the top so it aligns with the label, at node edge -->
262
249
  <Handle
263
250
  type="target"
264
251
  position={Position.Left}
265
252
  id={`${props.data.nodeId}-input-${port.id}`}
266
253
  class="flowdrop-workflow-node__handle"
267
- style="top: 50%; transform: translateY(-50%); --fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColorToken(
254
+ style="top: var(--fd-node-port-row-height); transform: translateY(-50%); --fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColorToken(
268
255
  checker,
269
256
  port.dataType
270
257
  )}); --fd-handle-border-color: var(--fd-handle-border);"
@@ -339,13 +326,13 @@
339
326
  {/if}
340
327
  </div>
341
328
 
342
- <!-- Output Handle: centered in row, at node edge (ports have no padding) -->
329
+ <!-- Output Handle: one grid row (20px) from the top so it aligns with the label, at node edge -->
343
330
  <Handle
344
331
  type="source"
345
332
  position={Position.Right}
346
333
  id={`${props.data.nodeId}-output-${port.id}`}
347
334
  class="flowdrop-workflow-node__handle"
348
- style="top: 50%; transform: translateY(-50%); --fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColorToken(
335
+ style="top: var(--fd-node-port-row-height); transform: translateY(-50%); --fd-handle-fill: var(--fd-port-skin-color, {getDataTypeColorToken(
349
336
  checker,
350
337
  port.dataType
351
338
  )}); --fd-handle-border-color: var(--fd-handle-border);"
@@ -358,22 +345,17 @@
358
345
  {/if}
359
346
 
360
347
  <!-- Config button -->
361
- <button
362
- class="flowdrop-workflow-node__config-btn"
363
- onclick={openConfigSidebar}
364
- title="Configure node"
365
- >
366
- <CogIcon />
367
- </button>
348
+ <NodeConfigButton onclick={openConfigSidebar} title="Configure node" />
368
349
  </div>
369
350
 
370
351
  <style>
371
352
  .flowdrop-workflow-node {
372
353
  position: relative;
373
- background-color: var(--fd-card);
374
- border: 1.5px solid var(--fd-node-border);
375
- border-radius: var(--fd-radius-xl);
376
- box-shadow: var(--fd-shadow-md);
354
+ background-color: var(--fd-node-bg);
355
+ backdrop-filter: var(--fd-node-backdrop-filter);
356
+ border: var(--fd-node-border-width) solid var(--fd-node-border);
357
+ border-radius: var(--fd-node-radius);
358
+ box-shadow: var(--fd-node-shadow);
377
359
  width: var(--fd-node-default-width);
378
360
  z-index: 10;
379
361
  color: var(--fd-foreground);
@@ -381,42 +363,49 @@
381
363
  }
382
364
 
383
365
  .flowdrop-workflow-node:hover {
384
- box-shadow: var(--fd-shadow-lg);
366
+ box-shadow: var(--fd-node-shadow-hover);
385
367
  border-color: var(--fd-node-border-hover);
386
368
  }
387
369
 
388
370
  .flowdrop-workflow-node--selected {
389
371
  box-shadow:
390
372
  0 0 0 2px var(--fd-primary-muted),
391
- var(--fd-shadow-lg);
373
+ var(--fd-node-shadow-hover);
392
374
  border-color: var(--fd-primary);
393
375
  }
394
376
 
395
377
  .flowdrop-workflow-node--selected:hover {
396
378
  box-shadow:
397
379
  0 0 0 2px var(--fd-primary-muted),
398
- var(--fd-shadow-lg);
380
+ var(--fd-node-shadow-hover);
399
381
  border-color: var(--fd-primary);
400
382
  }
401
383
 
402
- .flowdrop-workflow-node:focus-visible {
403
- outline: 2px solid var(--fd-ring);
404
- outline-offset: 2px;
405
- }
384
+ /* Focus ring is centralized in base.css (drawn on the .svelte-flow__node
385
+ wrapper, which is the focusable element). */
406
386
 
407
387
  .flowdrop-workflow-node__header {
408
388
  box-sizing: border-box;
409
- padding: var(--fd-node-header-gap) var(--fd-space-xl);
410
- border-bottom: 1px solid var(--fd-border-muted);
411
- background: var(--fd-header);
412
- border-top-left-radius: var(--fd-radius-xl);
413
- border-top-right-radius: var(--fd-radius-xl);
389
+ /* Bottom padding absorbs BOTH the node's own top border and the header
390
+ divider, so the body below the header lands on the 20px grid measured
391
+ from the node's outer top edge: node-border + header = 100/120/140. */
392
+ padding: var(--fd-node-header-gap) var(--fd-space-xl)
393
+ calc(
394
+ var(--fd-node-header-gap) - var(--fd-node-border-width) -
395
+ var(--fd-node-header-divider-width)
396
+ );
397
+ border-bottom: var(--fd-node-header-divider-width) solid var(--fd-node-header-divider-color);
398
+ background: var(--fd-node-header-bg);
399
+ border-top-left-radius: var(--fd-node-radius);
400
+ border-top-right-radius: var(--fd-node-radius);
414
401
  display: flex;
415
402
  flex-direction: column;
416
- gap: var(--fd-node-header-gap);
403
+ gap: calc(var(--fd-node-header-gap) * 2);
404
+ /* node-border (1.5) + header = 100/120/140. Header itself is
405
+ 4*gap + title + desc-line - node-border; each extra desc line adds 20. */
417
406
  min-height: calc(
418
- var(--fd-node-header-gap) * 2 + var(--fd-node-header-title-height) +
419
- var(--fd-node-header-desc-line)
407
+ var(--fd-node-header-gap) * 4 + var(--fd-node-header-title-height) +
408
+ var(--fd-node-header-desc-line) - var(--fd-node-border-width)
420
409
  );
421
410
  }
422
411
 
@@ -447,9 +436,10 @@
447
436
  display: var(--fd-node-icon-display, flex);
448
437
  align-items: center;
449
438
  justify-content: center;
450
- width: 2.25rem;
451
- height: 2.25rem;
452
- border-radius: 0.5rem;
439
+ /* px (not rem) so the icon stays grid-locked regardless of root font-size */
440
+ width: 36px;
441
+ height: 36px;
442
+ border-radius: 8px;
453
443
  background: color-mix(in srgb, var(--_icon-color) var(--fd-node-icon-bg-opacity), transparent);
454
444
  flex-shrink: 0;
455
445
  transition: all var(--fd-transition-normal);
@@ -465,8 +455,8 @@
465
455
  }
466
456
 
467
457
  .flowdrop-workflow-node__icon-wrapper :global(.flowdrop-workflow-node__icon) {
468
- width: 1.25rem;
469
- height: 1.25rem;
458
+ width: 20px;
459
+ height: 20px;
470
460
  color: var(--fd-node-icon);
471
461
  }
472
462
 
@@ -481,7 +471,14 @@
481
471
 
482
472
  .flowdrop-workflow-node__header-title h3 {
483
473
  margin: 0;
484
- line-height: 1;
474
+ /* half the title block so two lines fill it exactly on the 20px grid */
475
+ line-height: calc(var(--fd-node-header-title-height) / 2);
476
+ display: -webkit-box;
477
+ -webkit-line-clamp: 2;
478
+ line-clamp: 2;
479
+ -webkit-box-orient: vertical;
480
+ overflow: hidden;
481
+ min-width: 0;
485
482
  }
486
483
 
487
484
  @keyframes pulse {
@@ -501,27 +498,52 @@
501
498
  .flowdrop-workflow-node__ports-list {
502
499
  display: flex;
503
500
  flex-direction: column;
504
- gap: var(--fd-node-header-gap);
505
- padding: var(--fd-node-header-gap) 0;
501
+ gap: 0;
502
+ /* No vertical padding: sections stack flush and node height stays a
503
+ multiple of 20. The one exception is the clearance below the header
504
+ divider, applied to the first section only (below). */
505
+ padding: 0;
506
+ }
507
+
508
+ /* The first port section sits directly below the header divider; give it a
509
+ full 20px grid row of clearance so the first port lands on the grid. */
510
+ .flowdrop-workflow-node__header
511
+ + .flowdrop-workflow-node__ports
512
+ .flowdrop-workflow-node__ports-list {
513
+ padding-top: calc(var(--fd-node-header-gap) * 2);
506
514
  }
507
515
 
508
516
  .flowdrop-workflow-node__port {
509
517
  display: flex;
510
- align-items: center;
518
+ align-items: flex-start;
511
519
  gap: 0;
512
- min-height: var(--fd-node-port-row-height);
513
- padding: var(--fd-space-3xs) 0;
520
+ /* Fixed three-row (60px) height for every port — node height stays
521
+ predictable whether or not a port carries a description. */
522
+ height: calc(var(--fd-node-port-row-height) * 3);
523
+ padding: 0;
514
524
  position: relative;
515
525
  }
516
526
 
517
527
  .flowdrop-workflow-node__port-content {
518
- padding: 0 var(--fd-space-xl);
528
+ padding: var(--fd-node-header-gap) var(--fd-space-xl) 0;
529
+ }
530
+
531
+ /* Each line in a port occupies one 20px grid row: a label-only port
532
+ centers its single row, a label + description fills both. */
533
+ .flowdrop-workflow-node__port-content > div {
534
+ min-height: var(--fd-node-port-row-height);
535
+ align-items: center;
536
+ }
537
+
538
+ .flowdrop-workflow-node__port-content > p {
539
+ min-height: var(--fd-node-port-row-height);
540
+ line-height: var(--fd-node-port-row-height);
519
541
  }
520
542
 
521
543
  .flowdrop-badge {
522
- padding: 0.125rem var(--fd-space-3xs);
544
+ padding: 2px 4px;
523
545
  border-radius: var(--fd-radius-sm);
524
- font-size: 0.625rem;
546
+ font-size: 10px;
525
547
  font-weight: 500;
526
548
  text-transform: uppercase;
527
549
  letter-spacing: 0.05em;
@@ -533,8 +555,8 @@
533
555
  }
534
556
 
535
557
  .flowdrop-badge--sm {
536
- font-size: 0.625rem;
537
- padding: 0.125rem var(--fd-space-3xs);
558
+ font-size: 10px;
559
+ padding: 2px 4px;
538
560
  }
539
561
 
540
562
  /* Handle overrides: hover scale (base 20px/12px from base.css) */
@@ -569,12 +591,12 @@
569
591
 
570
592
  .flowdrop-text--xs {
571
593
  font-size: var(--fd-text-xs);
572
- line-height: 1rem;
594
+ line-height: 16px;
573
595
  }
574
596
 
575
597
  .flowdrop-text--sm {
576
598
  font-size: var(--fd-text-sm);
577
- line-height: 1.25rem;
599
+ line-height: 20px;
578
600
  }
579
601
 
580
602
  .flowdrop-text--gray {
@@ -595,39 +617,8 @@
595
617
  text-align: right;
596
618
  }
597
619
 
598
- .flowdrop-workflow-node__config-btn :global(svg) {
599
- width: 14px;
600
- height: 14px;
601
- }
602
-
603
- .flowdrop-workflow-node__config-btn {
604
- position: absolute;
605
- top: var(--fd-space-xs);
606
- right: var(--fd-space-xs);
607
- width: 1.5rem;
608
- height: 1.5rem;
609
- background-color: var(--fd-backdrop);
610
- border: 1px solid var(--fd-border);
611
- border-radius: var(--fd-radius-sm);
612
- color: var(--fd-muted-foreground);
613
- cursor: pointer;
614
- display: flex;
615
- align-items: center;
616
- justify-content: center;
617
- opacity: 0;
618
- transition: all var(--fd-transition-normal);
619
- backdrop-filter: var(--fd-backdrop-blur);
620
- z-index: 15;
621
- font-size: var(--fd-text-sm);
622
- }
623
-
624
- .flowdrop-workflow-node:hover .flowdrop-workflow-node__config-btn {
625
- opacity: 1;
626
- }
627
-
628
- .flowdrop-workflow-node__config-btn:hover {
629
- background-color: var(--fd-muted);
630
- border-color: var(--fd-border-strong);
631
- color: var(--fd-foreground);
620
+ /* Reveal the NodeConfigButton (gear) when the node is hovered. */
621
+ .flowdrop-workflow-node:hover {
622
+ --fd-config-btn-opacity: 1;
632
623
  }
633
624
  </style>