@d34dman/flowdrop 0.0.1 → 0.0.2

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 (119) hide show
  1. package/README.md +307 -215
  2. package/dist/adapters/WorkflowAdapter.d.ts +1 -1
  3. package/dist/adapters/WorkflowAdapter.js +30 -30
  4. package/dist/api/client.d.ts +24 -1
  5. package/dist/api/client.js +55 -38
  6. package/dist/api/enhanced-client.d.ts +46 -0
  7. package/dist/api/enhanced-client.js +211 -0
  8. package/dist/clients/ApiClient.d.ts +19 -23
  9. package/dist/clients/ApiClient.js +36 -34
  10. package/dist/components/App.svelte +1299 -230
  11. package/dist/components/App.svelte.d.ts +21 -1
  12. package/dist/components/CanvasBanner.svelte +50 -44
  13. package/dist/components/CanvasBanner.svelte.d.ts +5 -19
  14. package/dist/components/ConfigForm.svelte +555 -0
  15. package/dist/components/ConfigForm.svelte.d.ts +32 -0
  16. package/dist/components/ConfigModal.svelte +261 -0
  17. package/dist/components/ConfigModal.svelte.d.ts +31 -0
  18. package/dist/components/ConfigSidebar.svelte +934 -0
  19. package/dist/components/ConfigSidebar.svelte.d.ts +51 -0
  20. package/dist/components/ConnectionLine.svelte +32 -0
  21. package/dist/components/ConnectionLine.svelte.d.ts +3 -0
  22. package/dist/components/GatewayNode.svelte +471 -0
  23. package/dist/components/GatewayNode.svelte.d.ts +15 -0
  24. package/dist/components/LoadingSpinner.svelte +23 -23
  25. package/dist/components/LoadingSpinner.svelte.d.ts +1 -1
  26. package/dist/components/Logo.svelte +82 -0
  27. package/dist/components/Logo.svelte.d.ts +26 -0
  28. package/dist/components/LogsSidebar.svelte +565 -0
  29. package/dist/components/LogsSidebar.svelte.d.ts +34 -0
  30. package/dist/components/MarkdownDisplay.svelte +28 -0
  31. package/dist/components/MarkdownDisplay.svelte.d.ts +7 -0
  32. package/dist/components/Navbar.svelte +663 -0
  33. package/dist/components/Navbar.svelte.d.ts +21 -0
  34. package/dist/components/NodeSidebar.svelte +629 -488
  35. package/dist/components/NodeSidebar.svelte.d.ts +1 -2
  36. package/dist/components/NodeStatusOverlay.svelte +327 -0
  37. package/dist/components/NodeStatusOverlay.svelte.d.ts +11 -0
  38. package/dist/components/NotesNode.svelte +566 -0
  39. package/dist/components/NotesNode.svelte.d.ts +43 -0
  40. package/dist/components/PipelineStatus.svelte +331 -0
  41. package/dist/components/PipelineStatus.svelte.d.ts +18 -0
  42. package/dist/components/SimpleNode.svelte +447 -0
  43. package/dist/components/SimpleNode.svelte.d.ts +24 -0
  44. package/dist/components/SquareNode.svelte +346 -0
  45. package/dist/components/SquareNode.svelte.d.ts +24 -0
  46. package/dist/components/StatusIcon.svelte +112 -0
  47. package/dist/components/StatusIcon.svelte.d.ts +10 -0
  48. package/dist/components/StatusLabel.svelte +33 -0
  49. package/dist/components/StatusLabel.svelte.d.ts +7 -0
  50. package/dist/components/ToolNode.svelte +385 -0
  51. package/dist/components/ToolNode.svelte.d.ts +36 -0
  52. package/dist/components/UniversalNode.svelte +126 -0
  53. package/dist/components/UniversalNode.svelte.d.ts +15 -0
  54. package/dist/components/WorkflowEditor.svelte +871 -528
  55. package/dist/components/WorkflowEditor.svelte.d.ts +15 -5
  56. package/dist/components/WorkflowNode.svelte +428 -542
  57. package/dist/components/WorkflowNode.svelte.d.ts +7 -3
  58. package/dist/config/apiConfig.d.ts +33 -0
  59. package/dist/config/apiConfig.js +39 -0
  60. package/dist/config/defaultPortConfig.d.ts +6 -0
  61. package/dist/config/defaultPortConfig.js +192 -0
  62. package/dist/config/demo.d.ts +58 -0
  63. package/dist/config/demo.js +142 -0
  64. package/dist/config/endpoints.d.ts +106 -0
  65. package/dist/config/endpoints.js +128 -0
  66. package/dist/data/samples.d.ts +38 -4
  67. package/dist/data/samples.js +2789 -737
  68. package/dist/examples/adapter-usage.d.ts +4 -4
  69. package/dist/examples/adapter-usage.js +21 -26
  70. package/dist/examples/api-client-usage.d.ts +6 -6
  71. package/dist/examples/api-client-usage.js +55 -54
  72. package/dist/index.d.ts +23 -15
  73. package/dist/index.js +23 -15
  74. package/dist/mocks/app-environment.d.ts +8 -0
  75. package/dist/mocks/app-environment.js +16 -0
  76. package/dist/mocks/app-forms.d.ts +2 -0
  77. package/dist/mocks/app-forms.js +21 -0
  78. package/dist/mocks/app-navigation.d.ts +5 -0
  79. package/dist/mocks/app-navigation.js +34 -0
  80. package/dist/mocks/app-stores.d.ts +14 -0
  81. package/dist/mocks/app-stores.js +26 -0
  82. package/dist/services/api.d.ts +13 -3
  83. package/dist/services/api.js +91 -36
  84. package/dist/services/globalSave.d.ts +20 -0
  85. package/dist/services/globalSave.js +165 -0
  86. package/dist/services/nodeExecutionService.d.ts +63 -0
  87. package/dist/services/nodeExecutionService.js +261 -0
  88. package/dist/services/portConfigApi.d.ts +14 -0
  89. package/dist/services/portConfigApi.js +69 -0
  90. package/dist/services/toastService.d.ts +147 -0
  91. package/dist/services/toastService.js +235 -0
  92. package/dist/services/workflowStorage.d.ts +2 -2
  93. package/dist/services/workflowStorage.js +10 -10
  94. package/dist/stores/workflowStore.d.ts +53 -0
  95. package/dist/stores/workflowStore.js +264 -0
  96. package/dist/styles/base.css +896 -363
  97. package/dist/svelte-app.d.ts +52 -5
  98. package/dist/svelte-app.js +128 -6
  99. package/dist/types/config.d.ts +291 -0
  100. package/dist/types/config.js +4 -0
  101. package/dist/types/index.d.ts +231 -19
  102. package/dist/types/index.js +1 -1
  103. package/dist/utils/colors.d.ts +67 -33
  104. package/dist/utils/colors.js +183 -118
  105. package/dist/utils/config.d.ts +41 -0
  106. package/dist/utils/config.js +248 -0
  107. package/dist/utils/connections.d.ts +40 -3
  108. package/dist/utils/connections.js +115 -44
  109. package/dist/utils/icons.d.ts +1 -1
  110. package/dist/utils/icons.js +71 -70
  111. package/dist/utils/nodeStatus.d.ts +53 -0
  112. package/dist/utils/nodeStatus.js +183 -0
  113. package/dist/utils/nodeTypes.d.ts +57 -0
  114. package/dist/utils/nodeTypes.js +109 -0
  115. package/dist/utils/nodeWrapper.d.ts +39 -0
  116. package/dist/utils/nodeWrapper.js +62 -0
  117. package/package.json +129 -97
  118. package/dist/components/Node.svelte +0 -38
  119. package/dist/components/Node.svelte.d.ts +0 -4
@@ -5,496 +5,637 @@
5
5
  -->
6
6
 
7
7
  <script lang="ts">
8
- import type { NodeMetadata, NodeCategory } from "../types/index.js";
9
- import LoadingSpinner from "./LoadingSpinner.svelte";
10
- import Icon from "@iconify/svelte";
11
- import { getNodeIcon, getCategoryIcon, getDefaultIcon } from "../utils/icons.js";
12
- import { getCategoryColorToken } from "../utils/colors.js";
13
-
14
- interface Props {
15
- nodes: NodeMetadata[];
16
- selectedCategory?: NodeCategory;
17
- searchQuery?: string;
18
- }
19
-
20
- let props: Props = $props();
21
- let searchInput = $state("");
22
- let selectedCategory = $state(props.selectedCategory || "all");
23
-
24
- let filteredNodes = $derived(getFilteredNodes());
25
- let categories = $derived(getCategories());
26
-
27
- // Debug logging
28
- $effect(() => {
29
- console.log('NodeSidebar: props.nodes received:', props.nodes?.length || 0, 'nodes');
30
- console.log('NodeSidebar: props.nodes content:', props.nodes);
31
- });
32
-
33
- /**
34
- * Get all unique categories from node types
35
- */
36
- function getCategories(): NodeCategory[] {
37
- const nodes = props.nodes || [];
38
- if (nodes.length === 0) return [];
39
- const categories = new Set<NodeCategory>();
40
- nodes.forEach(node => categories.add(node.category));
41
- return Array.from(categories).sort();
42
- }
43
-
44
- /**
45
- * Filter node types based on search query and selected category
46
- */
47
- function getFilteredNodes(): NodeMetadata[] {
48
- // Use actual node types from props
49
- let filtered = props.nodes || [];
50
-
51
- // Filter by category
52
- if (selectedCategory !== "all") {
53
- filtered = filtered.filter(node => node.category === selectedCategory);
54
- }
55
-
56
- // Filter by search query
57
- if (searchInput.trim()) {
58
- const query = searchInput.toLowerCase();
59
- filtered = filtered.filter(node =>
60
- node.name.toLowerCase().includes(query) ||
61
- node.description.toLowerCase().includes(query) ||
62
- node.tags?.some(tag => tag.toLowerCase().includes(query))
63
- );
64
- }
65
-
66
- // Create a new array and sort it to avoid mutating the original
67
- return [...filtered].sort((a, b) => a.name.localeCompare(b.name));
68
- }
69
-
70
- /**
71
- * Handle node type drag start - creates a new node instance
72
- */
73
- function handleNodeDragStart(event: DragEvent, nodeType: NodeMetadata): void {
74
- if (!event.dataTransfer) return;
75
-
76
- // Create a new node instance from the node type
77
- const newNodeData = {
78
- type: "node",
79
- nodeType: nodeType.id,
80
- nodeData: {
81
- label: nodeType.name,
82
- config: { ...nodeType.configSchema },
83
- metadata: nodeType
84
- }
85
- };
86
-
87
- const jsonData = JSON.stringify(newNodeData);
88
-
89
- // Set the data that SvelteFlow will receive
90
- event.dataTransfer.setData("application/json", jsonData);
91
- event.dataTransfer.setData("text/plain", nodeType.name);
92
- event.dataTransfer.effectAllowed = "copy";
93
-
94
- // Set drag image
95
- if (event.target) {
96
- const rect = (event.target as HTMLElement).getBoundingClientRect();
97
- event.dataTransfer.setDragImage(event.target as HTMLElement, rect.width / 2, rect.height / 2);
98
- }
99
- }
100
-
101
- /**
102
- * Handle category selection
103
- */
104
- function handleCategorySelect(category: NodeCategory | "all"): void {
105
- selectedCategory = category;
106
- }
107
-
108
- /**
109
- * Handle search input change
110
- */
111
- function handleSearchChange(): void {
112
- // Search is handled reactively through the derived filteredNodes
113
- }
114
-
115
- /**
116
- * Handle node type click
117
- */
118
- function handleNodeClick(nodeType: NodeMetadata): void {
119
- // Node type selection is handled through events
120
- }
121
-
122
- /**
123
- * Get category display name
124
- */
125
- function getCategoryDisplayName(category: NodeCategory): string {
126
- const names: Record<NodeCategory, string> = {
127
- "inputs": "Inputs",
128
- "outputs": "Outputs",
129
- "prompts": "Prompts",
130
- "models": "Models",
131
- "processing": "Processing",
132
- "logic": "Logic",
133
- "data": "Data",
134
- "helpers": "Helpers",
135
- "tools": "Tools",
136
- "vector stores": "Vector Stores",
137
- "embeddings": "Embeddings",
138
- "memories": "Memories",
139
- "agents": "Agents",
140
- "bundles": "Bundles"
141
- };
142
- return names[category] || category;
143
- }
144
-
145
- /**
146
- * Get node type count for category
147
- */
148
- function getNodeCount(category: NodeCategory): number {
149
- const nodes = props.nodes || [];
150
- return nodes.filter(node => node.category === category).length;
151
- }
152
-
153
- /**
154
- * Get node types for category
155
- */
156
- function getNodesForCategory(category: NodeCategory): NodeMetadata[] {
157
- const nodes = props.nodes || [];
158
- return [...nodes]
159
- .filter(node => node.category === category)
160
- .sort((a, b) => a.name.localeCompare(b.name));
161
- }
162
-
163
- /**
164
- * Get filtered nodes for category
165
- */
166
- function getFilteredNodesForCategory(category: NodeCategory): NodeMetadata[] {
167
- let nodes = getNodesForCategory(category);
168
-
169
- // Filter by search query
170
- if (searchInput.trim()) {
171
- const query = searchInput.toLowerCase();
172
- nodes = nodes.filter(node =>
173
- node.name.toLowerCase().includes(query) ||
174
- node.description.toLowerCase().includes(query) ||
175
- node.tags?.some(tag => tag.toLowerCase().includes(query))
176
- );
177
- }
178
-
179
- return nodes;
180
- }
8
+ import type { NodeMetadata, NodeCategory } from '../types/index.js';
9
+ import LoadingSpinner from './LoadingSpinner.svelte';
10
+ import Icon from '@iconify/svelte';
11
+ import { getNodeIcon, getCategoryIcon } from '../utils/icons.js';
12
+ import { getCategoryColorToken } from '../utils/colors.js';
13
+
14
+ interface Props {
15
+ nodes: NodeMetadata[];
16
+ selectedCategory?: NodeCategory;
17
+ }
18
+
19
+ let props: Props = $props();
20
+ let searchInput = $state('');
21
+ let selectedCategory = $state(props.selectedCategory || 'all');
22
+
23
+ let filteredNodes = $derived(getFilteredNodes());
24
+ let categories = $derived(getCategories());
25
+
26
+ /**
27
+ * Get all unique categories from node types
28
+ */
29
+ function getCategories(): NodeCategory[] {
30
+ const nodes = props.nodes || [];
31
+ if (nodes.length === 0) return [];
32
+ const categories = new Set<NodeCategory>();
33
+ nodes.forEach((node) => categories.add(node.category));
34
+ return Array.from(categories).sort();
35
+ }
36
+
37
+ /**
38
+ * Filter node types based on search query and selected category
39
+ */
40
+ function getFilteredNodes(): NodeMetadata[] {
41
+ // Use actual node types from props
42
+ let filtered = props.nodes || [];
43
+
44
+ // Filter by category
45
+ if (selectedCategory !== 'all') {
46
+ filtered = filtered.filter((node) => node.category === selectedCategory);
47
+ }
48
+
49
+ // Filter by search query
50
+ if (searchInput.trim()) {
51
+ const query = searchInput.toLowerCase();
52
+ filtered = filtered.filter(
53
+ (node) =>
54
+ node.name.toLowerCase().includes(query) ||
55
+ node.description.toLowerCase().includes(query) ||
56
+ node.tags?.some((tag) => tag.toLowerCase().includes(query))
57
+ );
58
+ }
59
+
60
+ // Create a new array and sort it to avoid mutating the original
61
+ return [...filtered].sort((a, b) => a.name.localeCompare(b.name));
62
+ }
63
+
64
+ /**
65
+ * Handle node type drag start - creates a new node instance
66
+ */
67
+ function handleNodeDragStart(event: DragEvent, nodeType: NodeMetadata): void {
68
+ if (!event.dataTransfer) return;
69
+
70
+ // Extract initial config from configSchema with proper null checks
71
+ let initialConfig = {};
72
+ if (
73
+ nodeType.configSchema &&
74
+ typeof nodeType.configSchema === 'object' &&
75
+ nodeType.configSchema.properties &&
76
+ typeof nodeType.configSchema.properties === 'object'
77
+ ) {
78
+ // JSON Schema format - extract defaults
79
+ Object.entries(nodeType.configSchema.properties).forEach(([key, prop]) => {
80
+ if (prop && typeof prop === 'object' && 'default' in prop) {
81
+ initialConfig[key] = prop.default;
82
+ }
83
+ });
84
+ }
85
+
86
+ // Create a new node instance from the node type
87
+ const newNodeData = {
88
+ type: 'node',
89
+ nodeType: nodeType.id,
90
+ nodeData: {
91
+ label: nodeType.name,
92
+ config: initialConfig,
93
+ metadata: nodeType
94
+ }
95
+ };
96
+
97
+ const jsonData = JSON.stringify(newNodeData);
98
+
99
+ // Set the data that SvelteFlow will receive
100
+ event.dataTransfer.setData('application/json', jsonData);
101
+ event.dataTransfer.setData('text/plain', nodeType.name);
102
+ event.dataTransfer.effectAllowed = 'copy';
103
+
104
+ // Set drag image
105
+ if (event.target) {
106
+ const rect = (event.target as HTMLElement).getBoundingClientRect();
107
+ event.dataTransfer.setDragImage(event.target as HTMLElement, rect.width / 2, rect.height / 2);
108
+ }
109
+ }
110
+
111
+ /**
112
+ * Handle search input change
113
+ */
114
+ function handleSearchChange(): void {
115
+ // Search is handled reactively through the derived filteredNodes
116
+ }
117
+
118
+ /**
119
+ * Handle node click
120
+ */
121
+ function handleNodeClick(nodeType: NodeMetadata): void {
122
+ // Handle node click - could be used for preview or configuration
123
+ console.log('Node clicked:', nodeType.name);
124
+ }
125
+
126
+ /**
127
+ * Get category display name
128
+ */
129
+ function getCategoryDisplayName(category: NodeCategory): string {
130
+ const names: Record<NodeCategory, string> = {
131
+ inputs: 'Inputs',
132
+ outputs: 'Outputs',
133
+ prompts: 'Prompts',
134
+ models: 'Models',
135
+ processing: 'Processing',
136
+ logic: 'Logic',
137
+ data: 'Data',
138
+ tools: 'Tools',
139
+ helpers: 'Helpers',
140
+ 'vector stores': 'Vector Stores',
141
+ embeddings: 'Embeddings',
142
+ memories: 'Memories',
143
+ agents: 'Agents',
144
+ ai: 'AI',
145
+ bundles: 'Bundles'
146
+ };
147
+ return names[category] || category;
148
+ }
149
+
150
+ /**
151
+ * Get node types for category
152
+ */
153
+ function getNodesForCategory(category: NodeCategory): NodeMetadata[] {
154
+ const nodes = props.nodes || [];
155
+ return [...nodes]
156
+ .filter((node) => node.category === category)
157
+ .sort((a, b) => a.name.localeCompare(b.name));
158
+ }
159
+
160
+ /**
161
+ * Get filtered nodes for category
162
+ */
163
+ function getFilteredNodesForCategory(category: NodeCategory): NodeMetadata[] {
164
+ let nodes = getNodesForCategory(category);
165
+
166
+ // Filter by search query
167
+ if (searchInput.trim()) {
168
+ const query = searchInput.toLowerCase();
169
+ nodes = nodes.filter(
170
+ (node) =>
171
+ node.name.toLowerCase().includes(query) ||
172
+ node.description.toLowerCase().includes(query) ||
173
+ node.tags?.some((tag) => tag.toLowerCase().includes(query))
174
+ );
175
+ }
176
+
177
+ return nodes;
178
+ }
181
179
  </script>
182
180
 
183
- <div class="flowdrop-sidebar">
184
- <!-- Header -->
185
- <div class="flowdrop-sidebar__header">
186
- <div class="flowdrop-sidebar__title">
187
- <h2 class="flowdrop-text--lg flowdrop-font--bold">Components</h2>
188
- </div>
189
- </div>
190
-
191
- <!-- Search Section -->
192
- <div class="flowdrop-sidebar__search">
193
- <div class="flowdrop-join flowdrop-w--full">
194
- <div class="flowdrop-join__item flowdrop-flex--1">
195
- <input
196
- type="text"
197
- placeholder="Search components..."
198
- class="flowdrop-input flowdrop-join__item flowdrop-w--full"
199
- bind:value={searchInput}
200
- oninput={handleSearchChange}
201
- />
202
- </div>
203
- <button class="flowdrop-btn flowdrop-join__item" aria-label="Search components">
204
- <svg class="flowdrop-icon" fill="none" stroke="currentColor" viewBox="0 0 24 24">
205
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"></path>
206
- </svg>
207
- </button>
208
- </div>
209
- </div>
210
-
211
- <!-- Node Types List -->
212
- <div class="flowdrop-sidebar__content">
213
- {#if props.nodes?.length === 0}
214
- <!-- No node types available -->
215
- <div class="flowdrop-hero">
216
- <div class="flowdrop-hero__content">
217
- <div class="flowdrop-hero__icon">📦</div>
218
- <h3 class="flowdrop-hero__title">No node types available</h3>
219
- <p class="flowdrop-hero__description">Node type definitions will appear here</p>
220
- <div class="flowdrop-mb--4">
221
- <LoadingSpinner size="md" text="Loading from server..." />
222
- </div>
223
- </div>
224
- </div>
225
- {:else if searchInput.trim()}
226
- <!-- Search Results -->
227
- <div class="flowdrop-p--4">
228
- <div class="flowdrop-divider">
229
- <h3 class="flowdrop-divider__text">Search Results</h3>
230
- </div>
231
- {#if filteredNodes.length === 0}
232
- <div class="flowdrop-hero">
233
- <div class="flowdrop-hero__content">
234
- <div class="flowdrop-hero__icon">🔍</div>
235
- <h3 class="flowdrop-hero__title">No components found</h3>
236
- <p class="flowdrop-hero__description">Try adjusting your search</p>
237
- {#if props.nodes?.length === 0}
238
- <div class="flowdrop-mb--4">
239
- <LoadingSpinner size="sm" text="Loading components..." />
240
- </div>
241
- {/if}
242
- </div>
243
- </div>
244
- {:else}
245
- <div class="flowdrop-node-list">
246
- {#each filteredNodes as nodeType (nodeType.id)}
247
- <div
248
- class="flowdrop-card flowdrop-card--compact flowdrop-node-item"
249
- draggable="true"
250
- ondragstart={(e) => handleNodeDragStart(e, nodeType)}
251
- onclick={() => handleNodeClick(nodeType)}
252
- role="button"
253
- tabindex="0"
254
- onkeydown={(e) => {
255
- if (e.key === "Enter" || e.key === " ") {
256
- e.preventDefault();
257
- handleNodeClick(nodeType);
258
- }
259
- }}
260
- >
261
- <div class="flowdrop-card__body flowdrop-p--1 flowdrop-py--1">
262
- <div class="flowdrop-flex flowdrop-gap--2 flowdrop-items--center">
263
- <!-- Node Type Icon -->
264
- <div class="flowdrop-node-icon" style="background-color: {getCategoryColorToken(nodeType.category)}">
265
- <Icon icon={getNodeIcon(nodeType.icon, nodeType.category)} />
266
- </div>
267
-
268
- <!-- Node Type Info - Icon and Title only -->
269
- <h4 class="flowdrop-text--sm flowdrop-font--medium flowdrop-truncate flowdrop-flex--1">
270
- {nodeType.name}
271
- </h4>
272
- </div>
273
- <p class="flowdrop-text--xs flowdrop-text--gray flowdrop-truncate flowdrop-mt--1 flowdrop-ml--0">
274
- {nodeType.description}
275
- </p>
276
- </div>
277
- </div>
278
- {/each}
279
- </div>
280
- {/if}
281
- </div>
282
- {:else}
283
- <!-- Show categories with details when no search is active -->
284
- <div class="flowdrop-p--4">
285
-
286
- <!-- Category-specific details -->
287
- <div class="flowdrop-category-list">
288
- {#each categories as category (category)}
289
- {@const categoryNodes = getFilteredNodesForCategory(category)}
290
- {#if categoryNodes.length > 0}
291
- <details class="flowdrop-details">
292
- <summary class="flowdrop-details__summary">
293
- <div class="flowdrop-flex flowdrop-gap--2 flowdrop-items--center">
294
- <span class="flowdrop-node-icon" style="background-color: {getCategoryColorToken(category)}">
295
- <Icon icon={getCategoryIcon(category)} />
296
- </span>
297
- <span>{getCategoryDisplayName(category)}</span>
298
- </div>
299
- <div class="flowdrop-badge flowdrop-badge--secondary">{categoryNodes.length}</div>
300
- </summary>
301
- <div class="flowdrop-details__content">
302
- <div class="flowdrop-node-list">
303
- {#each categoryNodes as nodeType (nodeType.id)}
304
- <div
305
- class="flowdrop-card flowdrop-card--compact flowdrop-node-item"
306
- draggable="true"
307
- ondragstart={(e) => handleNodeDragStart(e, nodeType)}
308
- onclick={() => handleNodeClick(nodeType)}
309
- role="button"
310
- tabindex="0"
311
- onkeydown={(e) => {
312
- if (e.key === "Enter" || e.key === " ") {
313
- e.preventDefault();
314
- handleNodeClick(nodeType);
315
- }
316
- }}
317
- >
318
- <div class="flowdrop-card__body flowdrop-p--1 flowdrop-py--1">
319
- <div class="flowdrop-flex flowdrop-gap--2 flowdrop-items--center">
320
- <!-- Node Type Icon -->
321
- <div class="flowdrop-node-icon" style="background-color: {getCategoryColorToken(nodeType.category)}">
322
- <Icon icon={getNodeIcon(nodeType.icon, nodeType.category)} />
323
- </div>
324
-
325
- <!-- Node Type Info - Icon and Title only -->
326
- <h4 class="flowdrop-text--sm flowdrop-font--medium flowdrop-truncate flowdrop-flex--1">
327
- {nodeType.name}
328
- </h4>
329
- </div>
330
- <p class="flowdrop-text--xs flowdrop-text--gray flowdrop-truncate flowdrop-mt--1 flowdrop-ml--0">
331
- {nodeType.description}
332
- </p>
333
- </div>
334
- </div>
335
- {/each}
336
- </div>
337
- </div>
338
- </details>
339
- {/if}
340
- {/each}
341
- </div>
342
- </div>
343
- {/if}
344
- </div>
345
-
346
- <!-- Footer -->
347
- <div class="flowdrop-sidebar__footer">
348
- <div class="flowdrop-flex flowdrop-gap--4">
349
- <div class="flowdrop-flex flowdrop-gap--4">
350
- {#if props.nodes?.length === 0}
351
- <span class="flowdrop-text--xs flowdrop-text--gray">Loading components...</span>
352
- {:else}
353
- <span class="flowdrop-text--xs flowdrop-text--gray">Total: {props.nodes?.length || 0} components</span>
354
- <span class="flowdrop-text--xs flowdrop-text--gray">Showing: {filteredNodes.length}</span>
355
- {/if}
356
- </div>
357
- </div>
358
- </div>
181
+ <!-- Components Sidebar - Always Visible -->
182
+ <div
183
+ class="flowdrop-sidebar flowdrop-sidebar--container"
184
+ role="complementary"
185
+ aria-label="Components sidebar"
186
+ >
187
+ <!-- Header -->
188
+ <div class="flowdrop-sidebar__header">
189
+ <div class="flowdrop-sidebar__title">
190
+ <h2 class="flowdrop-text--lg flowdrop-font--bold">Components</h2>
191
+ </div>
192
+ </div>
193
+
194
+ <!-- Search Section -->
195
+ <div class="flowdrop-sidebar__search">
196
+ <div class="flowdrop-join flowdrop-w--full">
197
+ <div class="flowdrop-join__item flowdrop-flex--1">
198
+ <input
199
+ type="text"
200
+ placeholder="Search components..."
201
+ class="flowdrop-input flowdrop-join__item flowdrop-w--full"
202
+ bind:value={searchInput}
203
+ oninput={handleSearchChange}
204
+ />
205
+ </div>
206
+ <button class="flowdrop-btn flowdrop-join__item" aria-label="Search components">
207
+ <Icon icon="mdi:magnify" class="flowdrop-icon" />
208
+ </button>
209
+ </div>
210
+ </div>
211
+
212
+ <!-- Node Types List -->
213
+ <div class="flowdrop-sidebar__content">
214
+ {#if props.nodes?.length === 0}
215
+ <!-- No node types available -->
216
+ <div class="flowdrop-hero">
217
+ <div class="flowdrop-hero__content">
218
+ <div class="flowdrop-hero__icon">📦</div>
219
+ <h3 class="flowdrop-hero__title">No node types available</h3>
220
+ <p class="flowdrop-hero__description">Node type definitions will appear here</p>
221
+ <div class="flowdrop-mb--4">
222
+ <LoadingSpinner size="md" text="Loading from server..." />
223
+ </div>
224
+ </div>
225
+ </div>
226
+ {:else if searchInput.trim()}
227
+ <!-- Search Results -->
228
+ <div class="flowdrop-p--4">
229
+ <div class="flowdrop-divider">
230
+ <h3 class="flowdrop-divider__text">Search Results</h3>
231
+ </div>
232
+ {#if filteredNodes.length === 0}
233
+ <div class="flowdrop-hero">
234
+ <div class="flowdrop-hero__content">
235
+ <div class="flowdrop-hero__icon">🔍</div>
236
+ <h3 class="flowdrop-hero__title">No components found</h3>
237
+ <p class="flowdrop-hero__description">Try adjusting your search</p>
238
+ {#if props.nodes?.length === 0}
239
+ <div class="flowdrop-mb--4">
240
+ <LoadingSpinner size="sm" text="Loading components..." />
241
+ </div>
242
+ {/if}
243
+ </div>
244
+ </div>
245
+ {:else}
246
+ <div class="flowdrop-node-list">
247
+ {#each filteredNodes as nodeType (nodeType.id)}
248
+ <div
249
+ class="flowdrop-card flowdrop-card--compact flowdrop-node-item"
250
+ draggable="true"
251
+ ondragstart={(e) => handleNodeDragStart(e, nodeType)}
252
+ role="button"
253
+ tabindex="0"
254
+ >
255
+ <div class="flowdrop-card__body flowdrop-p--1 flowdrop-py--1">
256
+ <div class="flowdrop-flex flowdrop-gap--2 flowdrop-items--center">
257
+ <!-- Node Type Icon -->
258
+ <div
259
+ class="flowdrop-node-icon"
260
+ style="background-color: {getCategoryColorToken(nodeType.category)}"
261
+ >
262
+ <Icon icon={getNodeIcon(nodeType.icon, nodeType.category)} />
263
+ </div>
264
+
265
+ <!-- Node Type Info - Icon and Title only -->
266
+ <h4
267
+ class="flowdrop-text--sm flowdrop-font--medium flowdrop-truncate flowdrop-flex--1"
268
+ >
269
+ {nodeType.name}
270
+ </h4>
271
+ </div>
272
+ <p
273
+ class="flowdrop-text--xs flowdrop-text--gray flowdrop-truncate flowdrop-mt--1 flowdrop-ml--0"
274
+ >
275
+ {nodeType.description}
276
+ </p>
277
+ </div>
278
+ </div>
279
+ {/each}
280
+ </div>
281
+ {/if}
282
+ </div>
283
+ {:else}
284
+ <!-- Show categories with details when no search is active -->
285
+ <div class="flowdrop-p--4">
286
+ <!-- Category-specific details -->
287
+ <div class="flowdrop-category-list">
288
+ {#each categories as category (category)}
289
+ {@const categoryNodes = getFilteredNodesForCategory(category)}
290
+ {#if categoryNodes.length > 0}
291
+ <details class="flowdrop-details">
292
+ <summary class="flowdrop-details__summary">
293
+ <div class="flowdrop-flex flowdrop-gap--2 flowdrop-items--center">
294
+ <span
295
+ class="flowdrop-node-icon"
296
+ style="background-color: {getCategoryColorToken(category)}"
297
+ >
298
+ <Icon icon={getCategoryIcon(category)} />
299
+ </span>
300
+ <span>{getCategoryDisplayName(category)}</span>
301
+ </div>
302
+ <div class="flowdrop-badge flowdrop-badge--secondary">{categoryNodes.length}</div>
303
+ </summary>
304
+ <div class="flowdrop-details__content">
305
+ <div class="flowdrop-node-list">
306
+ {#each categoryNodes as nodeType (nodeType.id)}
307
+ <div
308
+ class="flowdrop-card flowdrop-card--compact flowdrop-node-item"
309
+ draggable="true"
310
+ ondragstart={(e) => handleNodeDragStart(e, nodeType)}
311
+ onclick={() => handleNodeClick(nodeType)}
312
+ role="button"
313
+ tabindex="0"
314
+ onkeydown={(e) => {
315
+ if (e.key === 'Enter' || e.key === ' ') {
316
+ e.preventDefault();
317
+ handleNodeClick(nodeType);
318
+ }
319
+ }}
320
+ >
321
+ <div class="flowdrop-card__body flowdrop-p--1 flowdrop-py--1">
322
+ <div class="flowdrop-flex flowdrop-gap--2 flowdrop-items--center">
323
+ <!-- Node Type Icon -->
324
+ <div
325
+ class="flowdrop-node-icon"
326
+ style="background-color: {getCategoryColorToken(nodeType.category)}"
327
+ >
328
+ <Icon icon={getNodeIcon(nodeType.icon, nodeType.category)} />
329
+ </div>
330
+
331
+ <!-- Node Type Info - Icon and Title only -->
332
+ <h4
333
+ class="flowdrop-text--sm flowdrop-font--medium flowdrop-truncate flowdrop-flex--1"
334
+ >
335
+ {nodeType.name}
336
+ </h4>
337
+ </div>
338
+ <p
339
+ class="flowdrop-text--xs flowdrop-text--gray flowdrop-truncate flowdrop-mt--1 flowdrop-ml--0"
340
+ >
341
+ {nodeType.description}
342
+ </p>
343
+ </div>
344
+ </div>
345
+ {/each}
346
+ </div>
347
+ </div>
348
+ </details>
349
+ {/if}
350
+ {/each}
351
+ </div>
352
+ </div>
353
+ {/if}
354
+ </div>
355
+
356
+ <!-- Footer -->
357
+ <div class="flowdrop-sidebar__footer">
358
+ <div class="flowdrop-flex flowdrop-gap--4">
359
+ <div class="flowdrop-flex flowdrop-gap--4">
360
+ {#if props.nodes?.length === 0}
361
+ <span class="flowdrop-text--xs flowdrop-text--gray">Loading components...</span>
362
+ {:else}
363
+ <span class="flowdrop-text--xs flowdrop-text--gray"
364
+ >Total: {props.nodes?.length || 0} components</span
365
+ >
366
+ <span class="flowdrop-text--xs flowdrop-text--gray">Showing: {filteredNodes.length}</span>
367
+ {/if}
368
+ </div>
369
+ </div>
370
+ </div>
359
371
  </div>
360
372
 
361
373
  <style>
362
- .flowdrop-sidebar {
363
- background-color: #f3f4f6;
364
- border-right: 1px solid #e5e7eb;
365
- width: 320px;
366
- height: 100%;
367
- display: flex;
368
- flex-direction: column;
369
- box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);
370
- }
371
-
372
- .flowdrop-sidebar__header {
373
- background-color: #ffffff;
374
- border-bottom: 1px solid #e5e7eb;
375
- padding: 0.5rem 1rem;
376
- }
377
-
378
- .flowdrop-sidebar__title {
379
- display: flex;
380
- align-items: center;
381
- }
382
-
383
- .flowdrop-sidebar__title h2 {
384
- font-size: 1rem;
385
- font-weight: 600;
386
- margin: 0;
387
- }
388
-
389
- .flowdrop-sidebar__search {
390
- padding: 0.75rem 1rem;
391
- background-color: #ffffff;
392
- border-bottom: 1px solid #e5e7eb;
393
- }
394
-
395
- .flowdrop-sidebar__content {
396
- flex: 1;
397
- overflow-y: auto;
398
- scrollbar-width: thin;
399
- scrollbar-color: #d1d5db #f3f4f6;
400
- }
401
-
402
- .flowdrop-sidebar__content::-webkit-scrollbar {
403
- width: 8px;
404
- }
405
-
406
- .flowdrop-sidebar__content::-webkit-scrollbar-track {
407
- background: #f3f4f6;
408
- }
409
-
410
- .flowdrop-sidebar__content::-webkit-scrollbar-thumb {
411
- background: #d1d5db;
412
- border-radius: 4px;
413
- }
414
-
415
- .flowdrop-sidebar__content::-webkit-scrollbar-thumb:hover {
416
- background: #9ca3af;
417
- }
418
-
419
- .flowdrop-sidebar__footer {
420
- background-color: rgba(255, 255, 255, 0.8);
421
- backdrop-filter: blur(8px);
422
- border-top: 1px solid #e5e7eb;
423
- padding: 0.5rem 0.75rem;
424
- }
425
-
426
- .flowdrop-node-list {
427
- display: flex;
428
- flex-direction: column;
429
- gap: 0.375rem;
430
- }
431
-
432
- .flowdrop-node-item {
433
- cursor: grab;
434
- transition: all 0.2s ease-in-out;
435
- border-radius: 0.375rem;
436
- border: 1px solid transparent;
437
- }
438
-
439
- .flowdrop-node-item:hover {
440
- border-color: #d1d5db;
441
- background-color: #f9fafb;
442
- }
443
-
444
- .flowdrop-node-item:active {
445
- cursor: grabbing;
446
- }
447
-
448
- .flowdrop-node-icon {
449
- width: 2rem;
450
- height: 2rem;
451
- border-radius: 0.375rem;
452
- color: #ffffff;
453
- font-size: 0.875rem;
454
- display: flex;
455
- align-items: center;
456
- justify-content: center;
457
- flex-shrink: 0;
458
- }
459
-
460
- .flowdrop-category-list {
461
- display: flex;
462
- flex-direction: column;
463
- gap: 0.375rem;
464
- }
465
-
466
- .flowdrop-icon {
467
- width: 1rem;
468
- height: 1rem;
469
- }
470
-
471
- .flowdrop-items--center {
472
- align-items: center;
473
- }
474
-
475
-
476
- .flowdrop-truncate {
477
- overflow: hidden;
478
- text-overflow: ellipsis;
479
- white-space: nowrap;
480
- }
481
-
482
- .flowdrop-card__body h4 {
483
- margin: 0;
484
- line-height: 1;
485
- }
486
-
487
- .flowdrop-p--4 {
488
- padding: 1rem;
489
- }
490
-
491
- .flowdrop-py--1 {
492
- padding-top: 0.25rem;
493
- padding-bottom: 0.25rem;
494
- }
495
-
496
- .flowdrop-ml--0 {
497
- margin-left: 0;
498
- }
499
-
500
- </style>
374
+ /* Components Sidebar - Always Visible */
375
+ .flowdrop-sidebar {
376
+ width: 320px;
377
+ height: calc(100vh - var(--flowdrop-navbar-height, 60px)); /* Account for navbar height */
378
+ background-color: #ffffff;
379
+ border-right: 1px solid #e5e7eb;
380
+ display: flex;
381
+ flex-direction: column;
382
+ box-shadow: 2px 0 8px rgba(0, 0, 0, 0.1);
383
+ flex-shrink: 0;
384
+ }
385
+
386
+ .flowdrop-sidebar--container {
387
+ display: flex;
388
+ flex-direction: column;
389
+ height: 100%;
390
+ }
391
+
392
+ .flowdrop-sidebar__header {
393
+ background-color: #f8fafc;
394
+ border-bottom: 1px solid #e5e7eb;
395
+ padding: 0.75rem 1rem;
396
+ display: flex;
397
+ align-items: center;
398
+ justify-content: space-between;
399
+ flex-shrink: 0;
400
+ }
401
+
402
+ .flowdrop-sidebar__title {
403
+ display: flex;
404
+ align-items: center;
405
+ }
406
+
407
+ /* Close button styles removed - no longer needed */
408
+
409
+ .flowdrop-sidebar__title h2 {
410
+ font-size: 1rem;
411
+ font-weight: 600;
412
+ margin: 0;
413
+ }
414
+
415
+ .flowdrop-sidebar__search {
416
+ padding: 0.75rem 1rem;
417
+ background-color: #ffffff;
418
+ border-bottom: 1px solid #e5e7eb;
419
+ }
420
+
421
+ .flowdrop-sidebar__content {
422
+ flex: 1;
423
+ overflow-y: scroll; /* Changed from auto to scroll to always show scrollbar */
424
+ scrollbar-width: thin;
425
+ scrollbar-color: #d1d5db #f3f4f6;
426
+ padding-bottom: 4rem; /* Add padding to ensure content is scrollable above footer */
427
+ min-height: 0; /* Allow flex item to shrink below content size */
428
+ }
429
+
430
+ .flowdrop-sidebar__content::-webkit-scrollbar {
431
+ width: 8px;
432
+ display: block; /* Ensure scrollbar is always visible */
433
+ }
434
+
435
+ .flowdrop-sidebar__content::-webkit-scrollbar-track {
436
+ background: #f3f4f6;
437
+ }
438
+
439
+ .flowdrop-sidebar__content::-webkit-scrollbar-thumb {
440
+ background: #d1d5db;
441
+ border-radius: 4px;
442
+ min-height: 20px; /* Ensure thumb has minimum height for visibility */
443
+ }
444
+
445
+ .flowdrop-sidebar__content::-webkit-scrollbar-thumb:hover {
446
+ background: #9ca3af;
447
+ }
448
+
449
+ .flowdrop-sidebar__footer {
450
+ background-color: rgba(255, 255, 255, 0.8);
451
+ backdrop-filter: blur(8px);
452
+ border-top: 1px solid #e5e7eb;
453
+ padding: 0.5rem 0.75rem;
454
+ flex-shrink: 0; /* Prevent footer from shrinking */
455
+ position: relative;
456
+ z-index: 10; /* Ensure footer stays on top */
457
+ height: 40px;
458
+ display: flex;
459
+ align-items: center;
460
+ }
461
+
462
+ .flowdrop-node-list {
463
+ display: flex;
464
+ flex-direction: column;
465
+ gap: 0.375rem;
466
+ }
467
+
468
+ .flowdrop-node-item {
469
+ cursor: grab;
470
+ transition: all 0.2s ease-in-out;
471
+ border-radius: 0.375rem;
472
+ border: 1px solid transparent;
473
+ }
474
+
475
+ .flowdrop-node-item:hover {
476
+ border-color: #d1d5db;
477
+ background-color: #f9fafb;
478
+ }
479
+
480
+ .flowdrop-node-item:active {
481
+ cursor: grabbing;
482
+ }
483
+
484
+ .flowdrop-node-icon {
485
+ width: 2rem;
486
+ height: 2rem;
487
+ border-radius: 0.375rem;
488
+ color: #ffffff;
489
+ font-size: 0.875rem;
490
+ display: flex;
491
+ align-items: center;
492
+ justify-content: center;
493
+ flex-shrink: 0;
494
+ }
495
+
496
+ .flowdrop-category-list {
497
+ display: flex;
498
+ flex-direction: column;
499
+ gap: 0.375rem;
500
+ }
501
+
502
+ .flowdrop-items--center {
503
+ align-items: center;
504
+ }
505
+
506
+ .flowdrop-truncate {
507
+ overflow: hidden;
508
+ text-overflow: ellipsis;
509
+ white-space: nowrap;
510
+ }
511
+
512
+ .flowdrop-card__body h4 {
513
+ margin: 0;
514
+ line-height: 1;
515
+ }
516
+
517
+ .flowdrop-p--4 {
518
+ padding: 1rem;
519
+ }
520
+
521
+ .flowdrop-py--1 {
522
+ padding-top: 0.25rem;
523
+ padding-bottom: 0.25rem;
524
+ }
525
+
526
+ .flowdrop-ml--0 {
527
+ margin-left: 0;
528
+ }
529
+
530
+ /* Form Elements - Matching App Design System */
531
+ .flowdrop-input {
532
+ padding: 0.625rem 0.75rem;
533
+ border: 1px solid #d1d5db;
534
+ border-radius: 0.375rem;
535
+ font-size: 0.875rem;
536
+ color: #111827;
537
+ background-color: #ffffff;
538
+ transition:
539
+ border-color 0.2s ease-in-out,
540
+ box-shadow 0.2s ease-in-out;
541
+ width: 100%;
542
+ height: 2.5rem;
543
+ box-sizing: border-box;
544
+ }
545
+
546
+ .flowdrop-input:focus {
547
+ outline: none;
548
+ border-color: #6b7280;
549
+ box-shadow: 0 0 0 3px rgba(107, 114, 128, 0.1);
550
+ }
551
+
552
+ .flowdrop-input::placeholder {
553
+ color: #9ca3af;
554
+ }
555
+
556
+ .flowdrop-btn {
557
+ padding: 0.625rem 0.75rem;
558
+ border-radius: 0.375rem;
559
+ font-size: 0.875rem;
560
+ font-weight: 500;
561
+ cursor: pointer;
562
+ transition: all 0.2s ease-in-out;
563
+ border: 1px solid #d1d5db;
564
+ display: flex;
565
+ align-items: center;
566
+ justify-content: center;
567
+ gap: 0.5rem;
568
+ background-color: #f9fafb;
569
+ color: #374151;
570
+ height: 2.5rem;
571
+ min-width: 2.5rem;
572
+ box-sizing: border-box;
573
+ }
574
+
575
+ .flowdrop-btn:hover {
576
+ background-color: #f3f4f6;
577
+ border-color: #9ca3af;
578
+ }
579
+
580
+ .flowdrop-btn:active {
581
+ background-color: #e5e7eb;
582
+ border-color: #6b7280;
583
+ }
584
+
585
+ .flowdrop-btn:focus {
586
+ outline: none;
587
+ box-shadow: 0 0 0 3px rgba(107, 114, 128, 0.1);
588
+ }
589
+
590
+ .flowdrop-btn:disabled {
591
+ background-color: #9ca3af;
592
+ border-color: #9ca3af;
593
+ cursor: not-allowed;
594
+ opacity: 0.6;
595
+ }
596
+
597
+ /* Join component styles - Seamless integration */
598
+ .flowdrop-join {
599
+ display: flex;
600
+ width: 100%;
601
+ border-radius: 0.375rem;
602
+ overflow: hidden;
603
+ border: 1px solid #d1d5db;
604
+ background-color: #ffffff;
605
+ }
606
+
607
+ .flowdrop-join__item {
608
+ border: none;
609
+ border-radius: 0;
610
+ background-color: transparent;
611
+ }
612
+
613
+ .flowdrop-join__item:first-child {
614
+ border-right: 1px solid #d1d5db;
615
+ flex: 1;
616
+ }
617
+
618
+ .flowdrop-join__item:last-child {
619
+ border-left: none;
620
+ background-color: #f9fafb;
621
+ color: #374151;
622
+ }
623
+
624
+ .flowdrop-join__item:last-child:hover {
625
+ background-color: #f3f4f6;
626
+ }
627
+
628
+ .flowdrop-join:focus-within {
629
+ border-color: #6b7280;
630
+ box-shadow: 0 0 0 3px rgba(107, 114, 128, 0.1);
631
+ }
632
+
633
+ /* Utility classes */
634
+ .flowdrop-w--full {
635
+ width: 100%;
636
+ }
637
+
638
+ .flowdrop-flex--1 {
639
+ flex: 1;
640
+ }
641
+ </style>