@flowdrop/flowdrop 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/App.svelte +80 -5
- package/dist/components/App.svelte.d.ts +7 -0
- package/dist/components/CanvasBanner.stories.svelte +6 -2
- package/dist/components/FlowDropEdge.svelte +7 -48
- package/dist/components/Logo.svelte +14 -14
- package/dist/components/Navbar.svelte +58 -10
- package/dist/components/Navbar.svelte.d.ts +7 -0
- package/dist/components/NodeSidebar.svelte +224 -314
- package/dist/components/nodes/SimpleNode.stories.svelte +64 -0
- package/dist/components/nodes/SimpleNode.svelte +27 -11
- package/dist/components/nodes/SquareNode.stories.svelte +45 -0
- package/dist/components/nodes/SquareNode.svelte +27 -11
- package/dist/components/nodes/WorkflowNode.stories.svelte +63 -0
- package/dist/skins/slate.js +16 -0
- package/dist/stories/EdgeDecorator.svelte +4 -4
- package/dist/styles/base.css +48 -0
- package/dist/svelte-app.d.ts +7 -1
- package/dist/svelte-app.js +4 -1
- package/package.json +1 -1
|
@@ -158,3 +158,67 @@
|
|
|
158
158
|
})}
|
|
159
159
|
/>
|
|
160
160
|
</Story>
|
|
161
|
+
|
|
162
|
+
<Story name="Dynamic Ports">
|
|
163
|
+
<NodeDecorator
|
|
164
|
+
data={createSampleNodeData({
|
|
165
|
+
label: "Custom Function",
|
|
166
|
+
config: {
|
|
167
|
+
dynamicInputs: [
|
|
168
|
+
{
|
|
169
|
+
name: "param_a",
|
|
170
|
+
label: "Parameter A",
|
|
171
|
+
description: "First parameter",
|
|
172
|
+
dataType: "string",
|
|
173
|
+
required: true,
|
|
174
|
+
},
|
|
175
|
+
{
|
|
176
|
+
name: "param_b",
|
|
177
|
+
label: "Parameter B",
|
|
178
|
+
description: "Second parameter",
|
|
179
|
+
dataType: "number",
|
|
180
|
+
required: false,
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
dynamicOutputs: [
|
|
184
|
+
{
|
|
185
|
+
name: "result",
|
|
186
|
+
label: "Result",
|
|
187
|
+
description: "Function result",
|
|
188
|
+
dataType: "json",
|
|
189
|
+
required: false,
|
|
190
|
+
},
|
|
191
|
+
],
|
|
192
|
+
},
|
|
193
|
+
metadata: {
|
|
194
|
+
id: "custom_function",
|
|
195
|
+
name: "Custom Function",
|
|
196
|
+
description: "A node with dynamic input and output ports",
|
|
197
|
+
category: "processing",
|
|
198
|
+
version: "1.0.0",
|
|
199
|
+
type: "simple",
|
|
200
|
+
supportedTypes: ["simple", "square", "default"],
|
|
201
|
+
icon: "mdi:function-variant",
|
|
202
|
+
color: "#8b5cf6",
|
|
203
|
+
inputs: [
|
|
204
|
+
{
|
|
205
|
+
id: "trigger",
|
|
206
|
+
name: "Trigger",
|
|
207
|
+
type: "input",
|
|
208
|
+
dataType: "trigger",
|
|
209
|
+
required: false,
|
|
210
|
+
},
|
|
211
|
+
],
|
|
212
|
+
outputs: [
|
|
213
|
+
{
|
|
214
|
+
id: "done",
|
|
215
|
+
name: "Done",
|
|
216
|
+
type: "output",
|
|
217
|
+
dataType: "trigger",
|
|
218
|
+
required: false,
|
|
219
|
+
},
|
|
220
|
+
],
|
|
221
|
+
},
|
|
222
|
+
})}
|
|
223
|
+
/>
|
|
224
|
+
</Story>
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
NodeMetadata,
|
|
17
17
|
NodeExtensions,
|
|
18
18
|
NodePort,
|
|
19
|
+
DynamicPort,
|
|
19
20
|
} from "../../types/index.js";
|
|
21
|
+
import { dynamicPortToNodePort } from "../../types/index.js";
|
|
20
22
|
import Icon from "@iconify/svelte";
|
|
21
23
|
import {
|
|
22
24
|
getDataTypeColor,
|
|
@@ -133,20 +135,34 @@
|
|
|
133
135
|
}
|
|
134
136
|
}
|
|
135
137
|
|
|
138
|
+
const dynamicInputs = $derived(
|
|
139
|
+
((props.data.config?.dynamicInputs as DynamicPort[]) || []).map((port) =>
|
|
140
|
+
dynamicPortToNodePort(port, "input"),
|
|
141
|
+
),
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
const dynamicOutputs = $derived(
|
|
145
|
+
((props.data.config?.dynamicOutputs as DynamicPort[]) || []).map((port) =>
|
|
146
|
+
dynamicPortToNodePort(port, "output"),
|
|
147
|
+
),
|
|
148
|
+
);
|
|
149
|
+
|
|
136
150
|
/**
|
|
137
151
|
* All visible input ports in user-defined order.
|
|
138
152
|
*/
|
|
139
153
|
const visibleInputPorts = $derived(
|
|
140
|
-
applyPortOrder(
|
|
141
|
-
(
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
),
|
|
154
|
+
applyPortOrder(
|
|
155
|
+
[...(props.data.metadata?.inputs ?? []), ...dynamicInputs],
|
|
156
|
+
portOrder.inputs,
|
|
157
|
+
).filter((p: NodePort) =>
|
|
158
|
+
isPortVisible(
|
|
159
|
+
p,
|
|
160
|
+
"input",
|
|
161
|
+
hiddenPorts,
|
|
162
|
+
hideUnconnectedHandles,
|
|
163
|
+
getConnectedHandles(),
|
|
164
|
+
props.data.nodeId,
|
|
165
|
+
),
|
|
150
166
|
),
|
|
151
167
|
);
|
|
152
168
|
|
|
@@ -155,7 +171,7 @@
|
|
|
155
171
|
*/
|
|
156
172
|
const visibleOutputPorts = $derived(
|
|
157
173
|
applyPortOrder(
|
|
158
|
-
props.data.metadata?.outputs ?? [],
|
|
174
|
+
[...(props.data.metadata?.outputs ?? []), ...dynamicOutputs],
|
|
159
175
|
portOrder.outputs,
|
|
160
176
|
).filter((p: NodePort) =>
|
|
161
177
|
isPortVisible(
|
|
@@ -80,3 +80,48 @@
|
|
|
80
80
|
selected={true}
|
|
81
81
|
/>
|
|
82
82
|
</Story>
|
|
83
|
+
|
|
84
|
+
<Story name="Dynamic Ports">
|
|
85
|
+
<NodeDecorator
|
|
86
|
+
data={createSampleNodeData({
|
|
87
|
+
label: "Data Mapper",
|
|
88
|
+
config: {
|
|
89
|
+
dynamicInputs: [
|
|
90
|
+
{
|
|
91
|
+
name: "source",
|
|
92
|
+
label: "Source Data",
|
|
93
|
+
dataType: "json",
|
|
94
|
+
required: true,
|
|
95
|
+
},
|
|
96
|
+
],
|
|
97
|
+
dynamicOutputs: [
|
|
98
|
+
{
|
|
99
|
+
name: "mapped",
|
|
100
|
+
label: "Mapped Output",
|
|
101
|
+
dataType: "json",
|
|
102
|
+
required: false,
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "errors",
|
|
106
|
+
label: "Errors",
|
|
107
|
+
dataType: "string",
|
|
108
|
+
required: false,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
metadata: {
|
|
113
|
+
id: "data_mapper",
|
|
114
|
+
name: "Data Mapper",
|
|
115
|
+
description: "Map data between formats",
|
|
116
|
+
category: "processing",
|
|
117
|
+
version: "1.0.0",
|
|
118
|
+
type: "square",
|
|
119
|
+
supportedTypes: ["square"],
|
|
120
|
+
icon: "mdi:swap-horizontal",
|
|
121
|
+
color: "#0ea5e9",
|
|
122
|
+
inputs: [],
|
|
123
|
+
outputs: [],
|
|
124
|
+
},
|
|
125
|
+
})}
|
|
126
|
+
/>
|
|
127
|
+
</Story>
|
|
@@ -16,7 +16,9 @@
|
|
|
16
16
|
NodeMetadata,
|
|
17
17
|
NodeExtensions,
|
|
18
18
|
NodePort,
|
|
19
|
+
DynamicPort,
|
|
19
20
|
} from "../../types/index.js";
|
|
21
|
+
import { dynamicPortToNodePort } from "../../types/index.js";
|
|
20
22
|
import Icon from "@iconify/svelte";
|
|
21
23
|
import {
|
|
22
24
|
getDataTypeColor,
|
|
@@ -117,20 +119,34 @@
|
|
|
117
119
|
openConfigSidebar();
|
|
118
120
|
}
|
|
119
121
|
}
|
|
122
|
+
const dynamicInputs = $derived(
|
|
123
|
+
((props.data.config?.dynamicInputs as DynamicPort[]) || []).map((port) =>
|
|
124
|
+
dynamicPortToNodePort(port, "input"),
|
|
125
|
+
),
|
|
126
|
+
);
|
|
127
|
+
|
|
128
|
+
const dynamicOutputs = $derived(
|
|
129
|
+
((props.data.config?.dynamicOutputs as DynamicPort[]) || []).map((port) =>
|
|
130
|
+
dynamicPortToNodePort(port, "output"),
|
|
131
|
+
),
|
|
132
|
+
);
|
|
133
|
+
|
|
120
134
|
/**
|
|
121
135
|
* All visible input ports in user-defined order.
|
|
122
136
|
*/
|
|
123
137
|
const visibleInputPorts = $derived(
|
|
124
|
-
applyPortOrder(
|
|
125
|
-
(
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
),
|
|
138
|
+
applyPortOrder(
|
|
139
|
+
[...(props.data.metadata?.inputs ?? []), ...dynamicInputs],
|
|
140
|
+
portOrder.inputs,
|
|
141
|
+
).filter((p: NodePort) =>
|
|
142
|
+
isPortVisible(
|
|
143
|
+
p,
|
|
144
|
+
"input",
|
|
145
|
+
hiddenPorts,
|
|
146
|
+
hideUnconnectedHandles,
|
|
147
|
+
getConnectedHandles(),
|
|
148
|
+
props.data.nodeId,
|
|
149
|
+
),
|
|
134
150
|
),
|
|
135
151
|
);
|
|
136
152
|
|
|
@@ -139,7 +155,7 @@
|
|
|
139
155
|
*/
|
|
140
156
|
const visibleOutputPorts = $derived(
|
|
141
157
|
applyPortOrder(
|
|
142
|
-
props.data.metadata?.outputs ?? [],
|
|
158
|
+
[...(props.data.metadata?.outputs ?? []), ...dynamicOutputs],
|
|
143
159
|
portOrder.outputs,
|
|
144
160
|
).filter((p: NodePort) =>
|
|
145
161
|
isPortVisible(
|
|
@@ -77,3 +77,66 @@
|
|
|
77
77
|
selected={true}
|
|
78
78
|
/>
|
|
79
79
|
</Story>
|
|
80
|
+
|
|
81
|
+
<Story name="Dynamic Ports">
|
|
82
|
+
<NodeDecorator
|
|
83
|
+
data={createSampleNodeData({
|
|
84
|
+
label: "Custom Function",
|
|
85
|
+
config: {
|
|
86
|
+
dynamicInputs: [
|
|
87
|
+
{
|
|
88
|
+
name: "input_1",
|
|
89
|
+
label: "First Input",
|
|
90
|
+
description: "The first input parameter",
|
|
91
|
+
dataType: "string",
|
|
92
|
+
required: true,
|
|
93
|
+
},
|
|
94
|
+
{
|
|
95
|
+
name: "input_2",
|
|
96
|
+
label: "Second Input",
|
|
97
|
+
description: "The second input parameter",
|
|
98
|
+
dataType: "number",
|
|
99
|
+
required: false,
|
|
100
|
+
},
|
|
101
|
+
],
|
|
102
|
+
dynamicOutputs: [
|
|
103
|
+
{
|
|
104
|
+
name: "output_1",
|
|
105
|
+
label: "Primary Output",
|
|
106
|
+
description: "The main output value",
|
|
107
|
+
dataType: "string",
|
|
108
|
+
required: false,
|
|
109
|
+
},
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
metadata: {
|
|
113
|
+
id: "custom-function",
|
|
114
|
+
name: "Custom Function",
|
|
115
|
+
description:
|
|
116
|
+
"Execute a custom function with dynamic inputs and outputs",
|
|
117
|
+
category: "processing",
|
|
118
|
+
version: "1.0.0",
|
|
119
|
+
type: "workflow",
|
|
120
|
+
icon: "mdi:function-variant",
|
|
121
|
+
inputs: [
|
|
122
|
+
{
|
|
123
|
+
id: "trigger",
|
|
124
|
+
name: "Trigger",
|
|
125
|
+
type: "input",
|
|
126
|
+
dataType: "trigger",
|
|
127
|
+
required: false,
|
|
128
|
+
},
|
|
129
|
+
],
|
|
130
|
+
outputs: [
|
|
131
|
+
{
|
|
132
|
+
id: "done",
|
|
133
|
+
name: "Done",
|
|
134
|
+
type: "output",
|
|
135
|
+
dataType: "trigger",
|
|
136
|
+
required: false,
|
|
137
|
+
},
|
|
138
|
+
],
|
|
139
|
+
},
|
|
140
|
+
})}
|
|
141
|
+
/>
|
|
142
|
+
</Story>
|
package/dist/skins/slate.js
CHANGED
|
@@ -42,6 +42,14 @@ export const slateSkin = {
|
|
|
42
42
|
"sidebar-category-color": "#c0c0d8",
|
|
43
43
|
// Flat item text: dark for light bg readability
|
|
44
44
|
"sidebar-flat-item-color": "#4a4a6a",
|
|
45
|
+
// Logo: monochrome purple-grey to match skin
|
|
46
|
+
"logo-bg": "#eeeef8",
|
|
47
|
+
"logo-stroke": "#5a5a7a",
|
|
48
|
+
"logo-line-fill": "#5a5a7a",
|
|
49
|
+
"logo-drop": "#009cde",
|
|
50
|
+
"logo-circle": "#f46351",
|
|
51
|
+
"logo-left": "#ccbaf4",
|
|
52
|
+
"logo-right": "#ffc423",
|
|
45
53
|
},
|
|
46
54
|
darkTokens: {
|
|
47
55
|
// --- Dark mode color palette (deep navy / original slate) ---
|
|
@@ -74,5 +82,13 @@ export const slateSkin = {
|
|
|
74
82
|
"sidebar-category-color": "#3a3a55",
|
|
75
83
|
// Flat item text: muted, not full-brightness white
|
|
76
84
|
"sidebar-flat-item-color": "#8888aa",
|
|
85
|
+
// Logo: monochrome purple-grey to match dark skin
|
|
86
|
+
"logo-bg": "none",
|
|
87
|
+
"logo-stroke": "#9898b8",
|
|
88
|
+
"logo-line-fill": "none",
|
|
89
|
+
"logo-drop": "none",
|
|
90
|
+
"logo-circle": "none",
|
|
91
|
+
"logo-left": "none",
|
|
92
|
+
"logo-right": "none",
|
|
77
93
|
},
|
|
78
94
|
};
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
const SOURCE_ID = "source-node";
|
|
47
47
|
const TARGET_ID = "target-node";
|
|
48
48
|
|
|
49
|
-
let nodes = $
|
|
49
|
+
let nodes = $derived<Node[]>([
|
|
50
50
|
{
|
|
51
51
|
id: SOURCE_ID,
|
|
52
52
|
type: "universalNode",
|
|
@@ -62,7 +62,7 @@
|
|
|
62
62
|
]);
|
|
63
63
|
|
|
64
64
|
// Handle IDs follow the format: {nodeId}-{input|output}-{portId}
|
|
65
|
-
let edges = $
|
|
65
|
+
let edges = $derived<Edge[]>([
|
|
66
66
|
{
|
|
67
67
|
id: "edge-1",
|
|
68
68
|
source: SOURCE_ID,
|
|
@@ -100,8 +100,8 @@
|
|
|
100
100
|
|
|
101
101
|
<div class="edge-decorator-wrapper">
|
|
102
102
|
<SvelteFlow
|
|
103
|
-
|
|
104
|
-
|
|
103
|
+
{nodes}
|
|
104
|
+
{edges}
|
|
105
105
|
{nodeTypes}
|
|
106
106
|
{edgeTypes}
|
|
107
107
|
fitView
|
package/dist/styles/base.css
CHANGED
|
@@ -30,6 +30,54 @@
|
|
|
30
30
|
box-shadow: var(--fd-shadow-md);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
+
/* xyflow Controls & MiniMap: wire up to skin tokens so themes can restyle them */
|
|
34
|
+
.svelte-flow {
|
|
35
|
+
--xy-controls-button-background-color: var(
|
|
36
|
+
--fd-controls-button-bg,
|
|
37
|
+
var(--fd-card)
|
|
38
|
+
);
|
|
39
|
+
--xy-controls-button-background-color-hover: var(
|
|
40
|
+
--fd-controls-button-bg-hover,
|
|
41
|
+
var(--fd-muted)
|
|
42
|
+
);
|
|
43
|
+
--xy-controls-button-color: var(
|
|
44
|
+
--fd-controls-button-color,
|
|
45
|
+
var(--fd-foreground)
|
|
46
|
+
);
|
|
47
|
+
--xy-controls-button-color-hover: var(
|
|
48
|
+
--fd-controls-button-color-hover,
|
|
49
|
+
var(--fd-foreground)
|
|
50
|
+
);
|
|
51
|
+
--xy-controls-button-border-color: var(
|
|
52
|
+
--fd-controls-button-border,
|
|
53
|
+
var(--fd-border)
|
|
54
|
+
);
|
|
55
|
+
--xy-controls-box-shadow: var(
|
|
56
|
+
--fd-controls-box-shadow,
|
|
57
|
+
0 0 2px 1px rgba(0, 0, 0, 0.08)
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
--xy-minimap-background-color: var(--fd-minimap-bg, var(--fd-card));
|
|
61
|
+
--xy-minimap-mask-background-color: var(
|
|
62
|
+
--fd-minimap-mask-bg,
|
|
63
|
+
var(--fd-backdrop)
|
|
64
|
+
);
|
|
65
|
+
--xy-minimap-mask-stroke-color: var(
|
|
66
|
+
--fd-minimap-mask-stroke,
|
|
67
|
+
var(--fd-border)
|
|
68
|
+
);
|
|
69
|
+
--xy-minimap-mask-stroke-width: var(--fd-minimap-mask-stroke-width, 1);
|
|
70
|
+
--xy-minimap-node-background-color: var(
|
|
71
|
+
--fd-minimap-node-bg,
|
|
72
|
+
var(--fd-muted)
|
|
73
|
+
);
|
|
74
|
+
--xy-minimap-node-stroke-color: var(
|
|
75
|
+
--fd-minimap-node-stroke,
|
|
76
|
+
var(--fd-border-muted)
|
|
77
|
+
);
|
|
78
|
+
--xy-minimap-node-stroke-width: var(--fd-minimap-node-stroke-width, 2);
|
|
79
|
+
}
|
|
80
|
+
|
|
33
81
|
/* Flow node handles: 20px connection area, 12px visible circle (::before)
|
|
34
82
|
Override xyflow's default background so port color (--fd-handle-fill from inline style) shows.
|
|
35
83
|
Use `.svelte-flow` parent for higher specificity (0-2-0) to beat xyflow defaults (0-1-0). */
|
package/dist/svelte-app.d.ts
CHANGED
|
@@ -13,7 +13,7 @@ import type { FlowDropEventHandlers, FlowDropFeatures } from "./types/events.js"
|
|
|
13
13
|
import type { FlowDropTheme, FlowDropThemeName } from "./types/theme.js";
|
|
14
14
|
import type { WorkflowFormatAdapter } from "./registry/workflowFormatRegistry.js";
|
|
15
15
|
import "./registry/builtinFormats.js";
|
|
16
|
-
import type { PartialSettings } from "./types/settings.js";
|
|
16
|
+
import type { PartialSettings, SettingsCategory } from "./types/settings.js";
|
|
17
17
|
/**
|
|
18
18
|
* Navbar action configuration
|
|
19
19
|
*/
|
|
@@ -74,6 +74,12 @@ export interface FlowDropMountOptions {
|
|
|
74
74
|
formatAdapters?: WorkflowFormatAdapter[];
|
|
75
75
|
/** Visual theme — named built-in ('default' | 'minimal') or custom theme object */
|
|
76
76
|
theme?: FlowDropTheme | FlowDropThemeName;
|
|
77
|
+
/** Which settings tabs to show in the modal (defaults to all) */
|
|
78
|
+
settingsCategories?: SettingsCategory[];
|
|
79
|
+
/** Show the "Sync to Cloud" button in the settings modal */
|
|
80
|
+
showSettingsSyncButton?: boolean;
|
|
81
|
+
/** Show the reset buttons in the settings modal */
|
|
82
|
+
showSettingsResetButton?: boolean;
|
|
77
83
|
}
|
|
78
84
|
/**
|
|
79
85
|
* Return type for mounted FlowDrop app
|
package/dist/svelte-app.js
CHANGED
|
@@ -47,7 +47,7 @@ import { globalSaveWorkflow, globalExportWorkflow, } from "./services/globalSave
|
|
|
47
47
|
* ```
|
|
48
48
|
*/
|
|
49
49
|
export async function mountFlowDropApp(container, options = {}) {
|
|
50
|
-
const { workflow, nodes, endpointConfig, portConfig, categories, height = "100vh", width = "100%", showNavbar = false, disableSidebar, lockWorkflow, readOnly, nodeStatuses, pipelineId, navbarTitle, navbarActions, showSettings, authProvider, eventHandlers, features: userFeatures, settings: initialSettings, draftStorageKey: customDraftKey, formatAdapters, theme, } = options;
|
|
50
|
+
const { workflow, nodes, endpointConfig, portConfig, categories, height = "100vh", width = "100%", showNavbar = false, disableSidebar, lockWorkflow, readOnly, nodeStatuses, pipelineId, navbarTitle, navbarActions, showSettings, authProvider, eventHandlers, features: userFeatures, settings: initialSettings, draftStorageKey: customDraftKey, formatAdapters, theme, settingsCategories, showSettingsSyncButton, showSettingsResetButton, } = options;
|
|
51
51
|
// Register custom format adapters before mounting
|
|
52
52
|
if (formatAdapters) {
|
|
53
53
|
for (const adapter of formatAdapters) {
|
|
@@ -137,6 +137,9 @@ export async function mountFlowDropApp(container, options = {}) {
|
|
|
137
137
|
eventHandlers,
|
|
138
138
|
features,
|
|
139
139
|
theme,
|
|
140
|
+
settingsCategories,
|
|
141
|
+
showSettingsSyncButton,
|
|
142
|
+
showSettingsResetButton,
|
|
140
143
|
},
|
|
141
144
|
});
|
|
142
145
|
// Set up draft auto-save manager
|
package/package.json
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
"description": "A drop-in visual workflow editor for any web application. You own the backend. You own the data. You own the orchestration.",
|
|
4
4
|
"license": "MIT",
|
|
5
5
|
"private": false,
|
|
6
|
-
"version": "1.
|
|
6
|
+
"version": "1.4.0",
|
|
7
7
|
"author": "Shibin Das (D34dMan)",
|
|
8
8
|
"bugs": {
|
|
9
9
|
"url": "https://github.com/flowdrop-io/flowdrop/issues"
|