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

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 (113) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/MIGRATION-2.0.md +13 -0
  3. package/README.md +5 -5
  4. package/dist/components/App.svelte +36 -191
  5. package/dist/components/App.svelte.d.ts +2 -7
  6. package/dist/components/Button.stories.svelte +65 -0
  7. package/dist/components/Button.stories.svelte.d.ts +19 -0
  8. package/dist/components/Button.svelte +62 -0
  9. package/dist/components/Button.svelte.d.ts +24 -0
  10. package/dist/components/CanvasIconButton.svelte +76 -0
  11. package/dist/components/CanvasIconButton.svelte.d.ts +18 -0
  12. package/dist/components/ConfigForm.svelte +4 -23
  13. package/dist/components/ConfigPanel.svelte +4 -3
  14. package/dist/components/EditorStatusBar.stories.svelte +44 -0
  15. package/dist/components/EditorStatusBar.stories.svelte.d.ts +27 -0
  16. package/dist/components/EditorStatusBar.svelte +99 -0
  17. package/dist/components/EditorStatusBar.svelte.d.ts +15 -0
  18. package/dist/components/IconButton.svelte +80 -0
  19. package/dist/components/IconButton.svelte.d.ts +30 -0
  20. package/dist/components/Input.svelte +74 -0
  21. package/dist/components/Input.svelte.d.ts +17 -0
  22. package/dist/components/LogoWordmark.svelte +113 -0
  23. package/dist/components/LogoWordmark.svelte.d.ts +26 -0
  24. package/dist/components/Navbar.svelte +17 -63
  25. package/dist/components/Navbar.svelte.d.ts +3 -0
  26. package/dist/components/NodeSidebar.svelte +17 -122
  27. package/dist/components/NodeSwapPicker.svelte +10 -28
  28. package/dist/components/PortMappingRow.svelte +0 -2
  29. package/dist/components/SchemaForm.svelte +0 -12
  30. package/dist/components/Select.svelte +53 -0
  31. package/dist/components/Select.svelte.d.ts +15 -0
  32. package/dist/components/SettingsModal.svelte +0 -5
  33. package/dist/components/SettingsPanel.svelte +2 -6
  34. package/dist/components/Textarea.svelte +39 -0
  35. package/dist/components/Textarea.svelte.d.ts +12 -0
  36. package/dist/components/ThemeToggle.svelte +15 -94
  37. package/dist/components/UniversalNode.svelte +32 -1
  38. package/dist/components/WorkflowEditor.svelte +62 -51
  39. package/dist/components/WorkflowEditor.svelte.d.ts +18 -0
  40. package/dist/components/chat/AIChatPanel.svelte +1 -1
  41. package/dist/components/console/ConsoleAutocomplete.svelte +1 -1
  42. package/dist/components/console/ConsoleOutput.svelte +2 -2
  43. package/dist/components/form/FormArray.svelte +37 -173
  44. package/dist/components/form/FormAutocomplete.svelte +10 -6
  45. package/dist/components/form/FormCheckboxGroup.svelte +1 -5
  46. package/dist/components/form/FormCodeEditor.svelte +9 -7
  47. package/dist/components/form/FormField.svelte +5 -44
  48. package/dist/components/form/FormFieldLight.svelte +8 -47
  49. package/dist/components/form/FormFieldset.svelte +1 -1
  50. package/dist/components/form/FormMarkdownEditor.svelte +8 -5
  51. package/dist/components/form/FormNumberField.svelte +4 -36
  52. package/dist/components/form/FormRangeField.svelte +18 -27
  53. package/dist/components/form/FormSelect.svelte +13 -75
  54. package/dist/components/form/FormTemplateEditor.svelte +6 -4
  55. package/dist/components/form/FormTextField.svelte +3 -35
  56. package/dist/components/form/FormTextarea.svelte +4 -39
  57. package/dist/components/form/FormToggle.svelte +0 -4
  58. package/dist/components/form/resolveFieldType.d.ts +24 -0
  59. package/dist/components/form/resolveFieldType.js +55 -0
  60. package/dist/components/icons/CloseIcon.svelte +6 -0
  61. package/dist/components/icons/CloseIcon.svelte.d.ts +26 -0
  62. package/dist/components/icons/CommandLineIcon.svelte +15 -0
  63. package/dist/components/icons/CommandLineIcon.svelte.d.ts +26 -0
  64. package/dist/components/icons/MenuIcon.svelte +4 -0
  65. package/dist/components/icons/MenuIcon.svelte.d.ts +26 -0
  66. package/dist/components/icons/MenuOpenIcon.svelte +6 -0
  67. package/dist/components/icons/MenuOpenIcon.svelte.d.ts +26 -0
  68. package/dist/components/interrupt/ChoicePrompt.svelte +0 -10
  69. package/dist/components/interrupt/ConfirmationPrompt.svelte +0 -5
  70. package/dist/components/interrupt/InterruptBubble.svelte +0 -10
  71. package/dist/components/interrupt/ReviewPrompt.svelte +0 -20
  72. package/dist/components/interrupt/TextInputPrompt.svelte +0 -6
  73. package/dist/components/layouts/MainLayout.svelte +4 -5
  74. package/dist/components/nodes/AtomNode.svelte +46 -34
  75. package/dist/components/nodes/GatewayNode.svelte +91 -99
  76. package/dist/components/nodes/IdeaNode.svelte +62 -90
  77. package/dist/components/nodes/NodeConfigButton.svelte +86 -0
  78. package/dist/components/nodes/NodeConfigButton.svelte.d.ts +15 -0
  79. package/dist/components/nodes/NotesNode.svelte +70 -81
  80. package/dist/components/nodes/SimpleNode.svelte +28 -78
  81. package/dist/components/nodes/SquareNode.svelte +79 -109
  82. package/dist/components/nodes/TerminalNode.svelte +28 -86
  83. package/dist/components/nodes/ToolNode.svelte +82 -95
  84. package/dist/components/nodes/WorkflowNode.svelte +91 -100
  85. package/dist/components/playground/ChatInput.svelte +0 -1
  86. package/dist/components/playground/InputCollector.svelte +11 -48
  87. package/dist/components/playground/PlaygroundApp.svelte +1 -1
  88. package/dist/components/playground/PlaygroundStudio.svelte +0 -5
  89. package/dist/messages/index.d.ts +1 -1
  90. package/dist/messages/index.js +1 -1
  91. package/dist/openapi/v1/openapi.yaml +2 -2
  92. package/dist/playground/mount.d.ts +9 -5
  93. package/dist/playground/mount.js +9 -5
  94. package/dist/skins/drafter.d.ts +30 -0
  95. package/dist/skins/drafter.js +198 -0
  96. package/dist/skins/index.d.ts +2 -1
  97. package/dist/skins/index.js +4 -2
  98. package/dist/styles/base.css +285 -14
  99. package/dist/styles/tokens.css +60 -2
  100. package/dist/svelte-app.d.ts +6 -0
  101. package/dist/svelte-app.js +71 -109
  102. package/dist/themes/drafter.d.ts +2 -0
  103. package/dist/themes/drafter.js +15 -0
  104. package/dist/themes/index.d.ts +2 -1
  105. package/dist/themes/index.js +8 -2
  106. package/dist/types/events.d.ts +18 -0
  107. package/dist/types/events.js +2 -1
  108. package/dist/types/settings.d.ts +1 -1
  109. package/dist/types/settings.js +1 -1
  110. package/dist/types/skin.d.ts +1 -1
  111. package/dist/types/theme.d.ts +16 -2
  112. package/dist/utils/connections.js +14 -50
  113. package/package.json +1 -1
@@ -561,11 +561,6 @@
561
561
  background-color: var(--fd-error-hover);
562
562
  }
563
563
 
564
- .interrupt-bubble__retry-btn:focus-visible {
565
- outline: 2px solid var(--fd-ring);
566
- outline-offset: 2px;
567
- }
568
-
569
564
  /* Body - prompt content area, full width */
570
565
  .interrupt-bubble__body {
571
566
  padding: var(--fd-space-xl);
@@ -648,11 +643,6 @@
648
643
  background-color: var(--fd-error-muted);
649
644
  }
650
645
 
651
- .interrupt-bubble__cancel-btn:focus-visible {
652
- outline: 2px solid var(--fd-ring);
653
- outline-offset: 2px;
654
- }
655
-
656
646
  .interrupt-bubble__cancel-btn:disabled {
657
647
  opacity: 0.5;
658
648
  cursor: not-allowed;
@@ -540,11 +540,6 @@
540
540
  color: var(--fd-error);
541
541
  }
542
542
 
543
- .review-prompt__bulk-btn:focus-visible {
544
- outline: 2px solid var(--fd-ring);
545
- outline-offset: 2px;
546
- }
547
-
548
543
  .review-prompt__bulk-btn:disabled {
549
544
  opacity: 0.5;
550
545
  cursor: not-allowed;
@@ -642,11 +637,6 @@
642
637
  color: var(--fd-error-foreground);
643
638
  }
644
639
 
645
- .review-prompt__toggle-btn:focus-visible {
646
- outline: 2px solid var(--fd-ring);
647
- outline-offset: 2px;
648
- }
649
-
650
640
  .review-prompt__toggle-btn:disabled {
651
641
  opacity: 0.5;
652
642
  cursor: not-allowed;
@@ -739,11 +729,6 @@
739
729
  border-color: var(--fd-border-strong);
740
730
  }
741
731
 
742
- .review-prompt__html-toggle-btn:focus-visible {
743
- outline: 2px solid var(--fd-ring);
744
- outline-offset: 2px;
745
- }
746
-
747
732
  /* Raw HTML code display */
748
733
  .review-prompt__raw-html {
749
734
  font-family: var(--fd-review-font-mono);
@@ -842,11 +827,6 @@
842
827
  transform: translateY(-1px);
843
828
  }
844
829
 
845
- .review-prompt__submit:focus-visible {
846
- outline: 2px solid var(--fd-ring);
847
- outline-offset: 2px;
848
- }
849
-
850
830
  .review-prompt__submit:disabled {
851
831
  opacity: 0.5;
852
832
  cursor: not-allowed;
@@ -254,7 +254,6 @@
254
254
  .text-prompt__input:focus,
255
255
  .text-prompt__textarea:focus {
256
256
  border-color: var(--fd-interrupt-completed-border);
257
- box-shadow: 0 0 0 3px var(--fd-interrupt-completed-shadow);
258
257
  }
259
258
 
260
259
  .text-prompt__input:disabled,
@@ -316,11 +315,6 @@
316
315
  transform: translateY(-1px);
317
316
  }
318
317
 
319
- .text-prompt__submit:focus-visible {
320
- outline: 2px solid var(--fd-ring);
321
- outline-offset: 2px;
322
- }
323
-
324
318
  .text-prompt__submit:disabled {
325
319
  opacity: 0.5;
326
320
  cursor: not-allowed;
@@ -510,12 +510,12 @@
510
510
 
511
511
  .flowdrop-main-layout__sidebar::-webkit-scrollbar-track {
512
512
  background: var(--fd-scrollbar-track);
513
- border-radius: 4px;
513
+ border-radius: var(--fd-scrollbar-radius);
514
514
  }
515
515
 
516
516
  .flowdrop-main-layout__sidebar::-webkit-scrollbar-thumb {
517
517
  background: var(--fd-scrollbar-thumb);
518
- border-radius: 4px;
518
+ border-radius: var(--fd-scrollbar-radius);
519
519
  }
520
520
 
521
521
  .flowdrop-main-layout__sidebar::-webkit-scrollbar-thumb:hover {
@@ -585,7 +585,6 @@
585
585
  }
586
586
 
587
587
  .flowdrop-main-layout__divider:focus {
588
- outline: none;
589
588
  background-color: var(--fd-primary-muted);
590
589
  }
591
590
 
@@ -669,12 +668,12 @@
669
668
 
670
669
  .flowdrop-main-layout__panel--bottom::-webkit-scrollbar-track {
671
670
  background: var(--fd-scrollbar-track);
672
- border-radius: 4px;
671
+ border-radius: var(--fd-scrollbar-radius);
673
672
  }
674
673
 
675
674
  .flowdrop-main-layout__panel--bottom::-webkit-scrollbar-thumb {
676
675
  background: var(--fd-scrollbar-thumb);
677
- border-radius: 4px;
676
+ border-radius: var(--fd-scrollbar-radius);
678
677
  }
679
678
 
680
679
  .flowdrop-main-layout__panel--bottom::-webkit-scrollbar-thumb:hover {
@@ -141,7 +141,7 @@
141
141
  return { text: atomCfg.placeholder ?? '', empty: true };
142
142
  });
143
143
 
144
- // Pill height is content-driven (~28px), so fixed px offsets don't fit.
144
+ // Pill is a fixed 40px tall (20px grid), so a single port centers at 20px.
145
145
  // Distribute handles as a % of node height: 50% for one port, evenly otherwise.
146
146
  function portTopPct(index: number, count: number): number {
147
147
  return ((index + 1) / (count + 1)) * 100;
@@ -150,12 +150,6 @@
150
150
  function openConfig(): void {
151
151
  data.onConfigOpen?.({ id: nodeId, type: nodeType, data });
152
152
  }
153
- function handleKeydown(event: KeyboardEvent): void {
154
- if (event.key === 'Enter' || event.key === ' ') {
155
- event.preventDefault();
156
- openConfig();
157
- }
158
- }
159
153
  </script>
160
154
 
161
155
  {#each inPorts as port, index (port.id)}
@@ -170,6 +164,11 @@
170
164
  />
171
165
  {/each}
172
166
 
167
+ <!-- Presentational: focus, keyboard and selection live on xyflow's node wrapper
168
+ (see UniversalNode, which maps Enter/Space to opening config). click is a
169
+ mouse convenience. -->
170
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
171
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
173
172
  <div
174
173
  class="flowdrop-atom-node"
175
174
  class:flowdrop-atom-node--selected={selected}
@@ -179,14 +178,13 @@
179
178
  class:flowdrop-atom-node--rect={isRect}
180
179
  style={nodeStyle}
181
180
  onclick={openConfig}
182
- onkeydown={handleKeydown}
183
- role="button"
184
- tabindex="0"
185
181
  >
186
- {#if atomCfg.prefix && !display.empty}
187
- <span class="flowdrop-atom-node__prefix" aria-hidden="true">{atomCfg.prefix}</span>
188
- {/if}
189
- <span class="flowdrop-atom-node__body" title={display.text}>{display.text}</span>
182
+ <div class="flowdrop-atom-node__pill">
183
+ {#if atomCfg.prefix && !display.empty}
184
+ <span class="flowdrop-atom-node__prefix" aria-hidden="true">{atomCfg.prefix}</span>
185
+ {/if}
186
+ <span class="flowdrop-atom-node__body" title={display.text}>{display.text}</span>
187
+ </div>
190
188
  </div>
191
189
 
192
190
  {#each outPorts as port, index (port.id)}
@@ -202,54 +200,68 @@
202
200
  {/each}
203
201
 
204
202
  <style>
203
+ /* Transparent slot: defines the node's bounding box so handles anchor
204
+ consistently, while the pill sits narrow and vertically centered inside. */
205
205
  .flowdrop-atom-node {
206
206
  position: relative;
207
- display: inline-flex;
207
+ box-sizing: border-box;
208
+ display: flex;
208
209
  align-items: center;
210
+ justify-content: center;
209
211
  width: fit-content;
210
- min-width: 2rem;
211
- min-height: 28px;
212
- padding: 2px var(--fd-space-sm);
213
- background-color: var(--fd-card);
212
+ /* 40px tall → a single port centers at 20px (50%); capped at 60px / 120px. */
213
+ min-height: 40px;
214
+ max-height: 60px;
215
+ max-width: 120px;
216
+ cursor: pointer;
217
+ z-index: 10;
218
+ }
219
+
220
+ /* The visible, themed pill — hugs its text and stays compact. */
221
+ .flowdrop-atom-node__pill {
222
+ box-sizing: border-box;
223
+ display: inline-flex;
224
+ align-items: center;
225
+ max-width: 100%;
226
+ min-width: 32px;
227
+ padding: 4px 10px;
228
+ background-color: var(--fd-node-bg);
229
+ backdrop-filter: var(--fd-node-backdrop-filter);
214
230
  /* --fd-atom-node-color is set inline only when the server provides a color;
215
231
  otherwise it falls back to the neutral border token. */
216
- border: 1.5px solid var(--fd-atom-node-color, var(--fd-node-border));
232
+ border: var(--fd-node-border-width) solid var(--fd-atom-node-color, var(--fd-node-border));
217
233
  border-radius: 999px;
218
234
  box-shadow: var(--fd-shadow-sm);
219
235
  color: var(--fd-foreground);
220
- cursor: pointer;
221
236
  transition:
222
237
  box-shadow var(--fd-transition-fast),
223
238
  border-color var(--fd-transition-fast);
224
- z-index: 10;
225
239
  }
226
240
 
227
- .flowdrop-atom-node--rect {
241
+ .flowdrop-atom-node--rect .flowdrop-atom-node__pill {
228
242
  border-radius: var(--fd-radius-md);
229
243
  }
230
244
 
231
- .flowdrop-atom-node:hover {
232
- box-shadow: var(--fd-shadow-md);
245
+ .flowdrop-atom-node:hover .flowdrop-atom-node__pill {
246
+ box-shadow: var(--fd-node-shadow);
233
247
  border-color: var(--fd-atom-node-color, var(--fd-node-border-hover));
234
248
  }
235
249
 
236
- .flowdrop-atom-node--selected {
250
+ .flowdrop-atom-node--selected .flowdrop-atom-node__pill {
237
251
  box-shadow:
238
252
  0 0 0 2px color-mix(in srgb, var(--fd-atom-node-color, var(--fd-primary)) 30%, transparent),
239
- var(--fd-shadow-md);
253
+ var(--fd-node-shadow);
240
254
  border-color: var(--fd-atom-node-color, var(--fd-primary));
241
255
  }
242
256
 
243
- .flowdrop-atom-node:focus-visible {
244
- outline: 2px solid var(--fd-ring);
245
- outline-offset: 2px;
246
- }
257
+ /* Focus ring is centralized in base.css (drawn on the .svelte-flow__node
258
+ wrapper, which is the focusable element). */
247
259
 
248
260
  .flowdrop-atom-node--processing {
249
261
  opacity: 0.7;
250
262
  }
251
263
 
252
- .flowdrop-atom-node--error {
264
+ .flowdrop-atom-node--error .flowdrop-atom-node__pill {
253
265
  border-color: var(--fd-error) !important;
254
266
  background-color: var(--fd-error-muted) !important;
255
267
  }
@@ -264,14 +276,14 @@
264
276
  margin-right: 2px;
265
277
  color: var(--fd-muted-foreground);
266
278
  font-size: var(--fd-text-sm);
267
- line-height: 1.2;
279
+ line-height: 20px;
268
280
  }
269
281
 
270
282
  .flowdrop-atom-node__body {
271
283
  /* min-width:0 lets the body ellipsize as a flex sibling of the prefix */
272
284
  min-width: 0;
273
285
  font-size: var(--fd-text-sm);
274
- line-height: 1.2;
286
+ line-height: 20px;
275
287
  white-space: nowrap;
276
288
  overflow: hidden;
277
289
  text-overflow: ellipsis;
@@ -12,6 +12,7 @@
12
12
  import { Position, Handle } from '@xyflow/svelte';
13
13
  import type { WorkflowNode, NodePort, Branch } from '../../types/index.js';
14
14
  import Icon from '@iconify/svelte';
15
+ import NodeConfigButton from './NodeConfigButton.svelte';
15
16
  import { getNodeIcon } from '../../utils/icons.js';
16
17
  import {
17
18
  getDataTypeColorToken,
@@ -120,13 +121,6 @@
120
121
  */
121
122
  const visibleBranches = $derived(branches.filter((branch) => isBranchVisible(branch.name)));
122
123
 
123
- /**
124
- * Handle node click - only handle selection, no config opening
125
- */
126
- function handleNodeClick(): void {
127
- // Node selection is handled by Svelte Flow
128
- }
129
-
130
124
  /**
131
125
  * Handle double-click to open config
132
126
  */
@@ -140,16 +134,6 @@
140
134
  }
141
135
  }
142
136
 
143
- /**
144
- * Handle keyboard events for accessibility
145
- */
146
- function handleKeydown(event: KeyboardEvent): void {
147
- if (event.key === 'Enter' || event.key === ' ') {
148
- event.preventDefault();
149
- handleNodeClick();
150
- }
151
- }
152
-
153
137
  /**
154
138
  * Check if a branch is active
155
139
  */
@@ -159,14 +143,13 @@
159
143
  </script>
160
144
 
161
145
  <!-- Node Container -->
146
+ <!-- Presentational: focus, keyboard and selection live on xyflow's node
147
+ wrapper (see UniversalNode). double-click is a mouse convenience. -->
148
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
162
149
  <div
163
150
  class="flowdrop-workflow-node flowdrop-workflow-node--gateway"
164
151
  class:flowdrop-workflow-node--selected={props.selected}
165
- onclick={handleNodeClick}
166
152
  ondblclick={handleNodeDoubleClick}
167
- onkeydown={handleKeydown}
168
- role="button"
169
- tabindex="0"
170
153
  aria-label={graph.gatewayNode({ title: displayTitle })}
171
154
  aria-describedby="node-description-{props.data.nodeId || 'unknown'}"
172
155
  >
@@ -185,7 +168,7 @@
185
168
  </div>
186
169
 
187
170
  <!-- Node Title - uses instanceTitle override if set -->
188
- <h3 class="flowdrop-text--sm flowdrop-font--medium flowdrop-truncate flowdrop-flex--1">
171
+ <h3 class="flowdrop-text--sm flowdrop-font--medium flowdrop-flex--1">
189
172
  {displayTitle}
190
173
  </h3>
191
174
  </div>
@@ -204,13 +187,13 @@
204
187
  <div class="flowdrop-workflow-node__ports-list">
205
188
  {#each visibleInputPorts as port (port.id)}
206
189
  <div class="flowdrop-workflow-node__port">
207
- <!-- Input Handle: centered in row, at node edge (ports have no padding) -->
190
+ <!-- Input Handle: one grid row (20px) from the top so it aligns with the label, at node edge -->
208
191
  <Handle
209
192
  type="target"
210
193
  position={Position.Left}
211
194
  id={`${props.data.nodeId}-input-${port.id}`}
212
195
  class="flowdrop-workflow-node__handle"
213
- style="top: 50%; transform: translateY(-50%); --fd-handle-fill: {getDataTypeColorToken(
196
+ style="top: var(--fd-node-port-row-height); transform: translateY(-50%); --fd-handle-fill: {getDataTypeColorToken(
214
197
  checker,
215
198
  port.dataType
216
199
  )}; --fd-handle-border-color: var(--fd-handle-border);"
@@ -293,13 +276,13 @@
293
276
  </div>
294
277
  </div>
295
278
 
296
- <!-- Output Handle: centered in row, at node edge (ports have no padding) -->
279
+ <!-- Output Handle: one grid row (20px) from the top so it aligns with the label, at node edge -->
297
280
  <Handle
298
281
  type="source"
299
282
  position={Position.Right}
300
283
  id={`${props.data.nodeId}-output-${branch.name}`}
301
284
  class={`flowdrop-workflow-node__handle ${isActive ? 'flowdrop-workflow-node__handle--active' : ''}`}
302
- style="top: 50%; transform: translateY(-50%); --fd-handle-fill: {isActive
285
+ style="top: var(--fd-node-port-row-height); transform: translateY(-50%); --fd-handle-fill: {isActive
303
286
  ? getDataTypeColorToken(checker, 'trigger')
304
287
  : getDataTypeColorToken(
305
288
  checker,
@@ -323,22 +306,17 @@
323
306
  <!-- Note: When all branches are hidden due to hideUnconnectedHandles, we don't show anything -->
324
307
 
325
308
  <!-- Config button -->
326
- <button
327
- class="flowdrop-workflow-node__config-btn"
328
- onclick={handleNodeDoubleClick}
329
- title="Configure node"
330
- >
331
- <Icon icon="mdi:cog" />
332
- </button>
309
+ <NodeConfigButton onclick={handleNodeDoubleClick} title="Configure node" />
333
310
  </div>
334
311
 
335
312
  <style>
336
313
  .flowdrop-workflow-node {
337
314
  position: relative;
338
- background-color: var(--fd-card);
339
- border: 1.5px solid var(--fd-node-border);
340
- border-radius: var(--fd-radius-xl);
341
- box-shadow: var(--fd-shadow-md);
315
+ background-color: var(--fd-node-bg);
316
+ backdrop-filter: var(--fd-node-backdrop-filter);
317
+ border: var(--fd-node-border-width) solid var(--fd-node-border);
318
+ border-radius: var(--fd-node-radius);
319
+ box-shadow: var(--fd-node-shadow);
342
320
  width: var(--fd-node-default-width);
343
321
  z-index: 10;
344
322
  color: var(--fd-foreground);
@@ -350,42 +328,49 @@
350
328
  }
351
329
 
352
330
  .flowdrop-workflow-node:hover {
353
- box-shadow: var(--fd-shadow-lg);
331
+ box-shadow: var(--fd-node-shadow-hover);
354
332
  border-color: var(--fd-node-border-hover);
355
333
  }
356
334
 
357
335
  .flowdrop-workflow-node--selected {
358
336
  box-shadow:
359
337
  0 0 0 2px var(--fd-primary-muted),
360
- var(--fd-shadow-lg);
338
+ var(--fd-node-shadow-hover);
361
339
  border-color: var(--fd-primary);
362
340
  }
363
341
 
364
342
  .flowdrop-workflow-node--selected:hover {
365
343
  box-shadow:
366
344
  0 0 0 2px var(--fd-primary-muted),
367
- var(--fd-shadow-lg);
345
+ var(--fd-node-shadow-hover);
368
346
  border-color: var(--fd-primary);
369
347
  }
370
348
 
371
- .flowdrop-workflow-node:focus-visible {
372
- outline: 2px solid var(--fd-ring);
373
- outline-offset: 2px;
374
- }
349
+ /* Focus ring is centralized in base.css (drawn on the .svelte-flow__node
350
+ wrapper, which is the focusable element). */
375
351
 
376
352
  .flowdrop-workflow-node__header {
377
353
  box-sizing: border-box;
378
- padding: var(--fd-node-header-gap) var(--fd-space-xl);
379
- border-bottom: 1px solid var(--fd-border-muted);
380
- background: var(--fd-header);
381
- border-top-left-radius: var(--fd-radius-xl);
382
- border-top-right-radius: var(--fd-radius-xl);
354
+ /* Bottom padding absorbs BOTH the node's own top border and the header
355
+ divider, so the body below the header lands on the 20px grid measured
356
+ from the node's outer top edge: node-border + header = 100/120/140. */
357
+ padding: var(--fd-node-header-gap) var(--fd-space-xl)
358
+ calc(
359
+ var(--fd-node-header-gap) - var(--fd-node-border-width) -
360
+ var(--fd-node-header-divider-width)
361
+ );
362
+ border-bottom: var(--fd-node-header-divider-width) solid var(--fd-node-header-divider-color);
363
+ background: var(--fd-node-header-bg);
364
+ border-top-left-radius: var(--fd-node-radius);
365
+ border-top-right-radius: var(--fd-node-radius);
383
366
  display: flex;
384
367
  flex-direction: column;
385
- gap: var(--fd-node-header-gap);
368
+ gap: calc(var(--fd-node-header-gap) * 2);
369
+ /* node-border (1.5) + header = 100/120/140. Header itself is
370
+ 4*gap + title + desc-line - node-border; each extra desc line adds 20. */
386
371
  min-height: calc(
387
- var(--fd-node-header-gap) * 2 + var(--fd-node-header-title-height) +
388
- var(--fd-node-header-desc-line)
372
+ var(--fd-node-header-gap) * 4 + var(--fd-node-header-title-height) +
373
+ var(--fd-node-header-desc-line) - var(--fd-node-border-width)
389
374
  );
390
375
  }
391
376
 
@@ -416,9 +401,10 @@
416
401
  display: flex;
417
402
  align-items: center;
418
403
  justify-content: center;
419
- width: 2.25rem;
420
- height: 2.25rem;
421
- border-radius: 0.5rem;
404
+ /* px (not rem) so the icon stays grid-locked regardless of root font-size */
405
+ width: 36px;
406
+ height: 36px;
407
+ border-radius: 8px;
422
408
  background: color-mix(in srgb, var(--_icon-color) var(--fd-node-icon-bg-opacity), transparent);
423
409
  flex-shrink: 0;
424
410
  transition: all var(--fd-transition-normal);
@@ -434,15 +420,22 @@
434
420
  }
435
421
 
436
422
  .flowdrop-workflow-node__icon-wrapper :global(.flowdrop-workflow-node__icon) {
437
- width: 1.25rem;
438
- height: 1.25rem;
423
+ width: 20px;
424
+ height: 20px;
439
425
  color: var(--fd-node-icon);
440
426
  }
441
427
 
442
428
  .flowdrop-workflow-node__header-title h3 {
443
429
  margin: 0;
444
- line-height: 1;
430
+ /* half the title block so two lines fill it exactly on the 20px grid */
431
+ line-height: calc(var(--fd-node-header-title-height) / 2);
445
432
  color: var(--fd-foreground);
433
+ display: -webkit-box;
434
+ -webkit-line-clamp: 2;
435
+ line-clamp: 2;
436
+ -webkit-box-orient: vertical;
437
+ overflow: hidden;
438
+ min-width: 0;
446
439
  }
447
440
 
448
441
  .flowdrop-workflow-node__ports {
@@ -452,27 +445,52 @@
452
445
  .flowdrop-workflow-node__ports-list {
453
446
  display: flex;
454
447
  flex-direction: column;
455
- gap: var(--fd-node-header-gap);
456
- padding: var(--fd-node-header-gap) 0;
448
+ gap: 0;
449
+ /* No vertical padding: sections stack flush and node height stays a
450
+ multiple of 20. The one exception is the clearance below the header
451
+ divider, applied to the first section only (below). */
452
+ padding: 0;
453
+ }
454
+
455
+ /* The first port section sits directly below the header divider; give it a
456
+ full 20px grid row of clearance so the first port lands on the grid. */
457
+ .flowdrop-workflow-node__header
458
+ + .flowdrop-workflow-node__ports
459
+ .flowdrop-workflow-node__ports-list {
460
+ padding-top: calc(var(--fd-node-header-gap) * 2);
457
461
  }
458
462
 
459
463
  .flowdrop-workflow-node__port {
460
464
  display: flex;
461
- align-items: center;
465
+ align-items: flex-start;
462
466
  gap: 0;
463
- min-height: var(--fd-node-port-row-height);
464
- padding: var(--fd-space-3xs) 0;
467
+ /* Fixed three-row (60px) height for every port — node height stays
468
+ predictable whether or not a port carries a description. */
469
+ height: calc(var(--fd-node-port-row-height) * 3);
470
+ padding: 0;
465
471
  position: relative;
466
472
  }
467
473
 
468
474
  .flowdrop-workflow-node__port-content {
469
- padding: 0 var(--fd-space-xl);
475
+ padding: var(--fd-node-header-gap) var(--fd-space-xl) 0;
476
+ }
477
+
478
+ /* Each line in a port occupies one 20px grid row: a label-only port
479
+ centers its single row, a label + description fills both. */
480
+ .flowdrop-workflow-node__port-content > div {
481
+ min-height: var(--fd-node-port-row-height);
482
+ align-items: center;
483
+ }
484
+
485
+ .flowdrop-workflow-node__port-content > p {
486
+ min-height: var(--fd-node-port-row-height);
487
+ line-height: var(--fd-node-port-row-height);
470
488
  }
471
489
 
472
490
  .flowdrop-badge {
473
- padding: 0.125rem var(--fd-space-3xs);
491
+ padding: 2px 4px;
474
492
  border-radius: var(--fd-radius-sm);
475
- font-size: 0.625rem;
493
+ font-size: 10px;
476
494
  font-weight: 500;
477
495
  text-transform: uppercase;
478
496
  letter-spacing: 0.05em;
@@ -484,8 +502,8 @@
484
502
  }
485
503
 
486
504
  .flowdrop-badge--sm {
487
- font-size: 0.625rem;
488
- padding: 0.125rem var(--fd-space-3xs);
505
+ font-size: 10px;
506
+ padding: 2px 4px;
489
507
  }
490
508
 
491
509
  .workflow-node__no-branches {
@@ -537,12 +555,12 @@
537
555
 
538
556
  .flowdrop-text--xs {
539
557
  font-size: var(--fd-text-xs);
540
- line-height: 1rem;
558
+ line-height: 16px;
541
559
  }
542
560
 
543
561
  .flowdrop-text--sm {
544
562
  font-size: var(--fd-text-sm);
545
- line-height: 1.25rem;
563
+ line-height: 20px;
546
564
  }
547
565
 
548
566
  .flowdrop-text--gray {
@@ -568,34 +586,8 @@
568
586
  text-align: right;
569
587
  }
570
588
 
571
- .flowdrop-workflow-node__config-btn {
572
- position: absolute;
573
- top: var(--fd-space-xs);
574
- right: var(--fd-space-xs);
575
- width: 1.5rem;
576
- height: 1.5rem;
577
- background-color: var(--fd-backdrop);
578
- border: 1px solid var(--fd-border);
579
- border-radius: var(--fd-radius-sm);
580
- color: var(--fd-muted-foreground);
581
- cursor: pointer;
582
- display: flex;
583
- align-items: center;
584
- justify-content: center;
585
- opacity: 0;
586
- transition: all var(--fd-transition-normal);
587
- backdrop-filter: blur(4px);
588
- z-index: 15;
589
- font-size: var(--fd-text-sm);
590
- }
591
-
592
- .flowdrop-workflow-node:hover .flowdrop-workflow-node__config-btn {
593
- opacity: 1;
594
- }
595
-
596
- .flowdrop-workflow-node__config-btn:hover {
597
- background-color: var(--fd-muted);
598
- border-color: var(--fd-border-strong);
599
- color: var(--fd-foreground);
589
+ /* Reveal the NodeConfigButton (gear) when the node is hovered. */
590
+ .flowdrop-workflow-node:hover {
591
+ --fd-config-btn-opacity: 1;
600
592
  }
601
593
  </style>