@d34dman/flowdrop 0.0.27 → 0.0.28

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.
@@ -0,0 +1,492 @@
1
+ <!--
2
+ SchemaForm Component
3
+
4
+ A standalone form generator that creates dynamic forms from JSON Schema definitions.
5
+ Designed for external use without coupling to FlowDrop workflow nodes.
6
+
7
+ Features:
8
+ - Dynamic form generation from JSON Schema
9
+ - Two-way binding with value updates via onChange callback
10
+ - Optional save/cancel action buttons
11
+ - Support for all standard JSON Schema field types
12
+ - Accessible form controls with ARIA attributes
13
+
14
+ @example
15
+ ```svelte
16
+ <script lang="ts">
17
+ import { SchemaForm } from "flowdrop";
18
+ import type { ConfigSchema } from "flowdrop";
19
+
20
+ const schema: ConfigSchema = {
21
+ type: "object",
22
+ properties: {
23
+ name: { type: "string", title: "Name", description: "Enter your name" },
24
+ age: { type: "number", title: "Age", minimum: 0, maximum: 120 },
25
+ active: { type: "boolean", title: "Active", default: true }
26
+ },
27
+ required: ["name"]
28
+ };
29
+
30
+ let values = $state({ name: "", age: 25, active: true });
31
+
32
+ function handleChange(newValues: Record<string, unknown>) {
33
+ values = newValues;
34
+ }
35
+
36
+ function handleSave(finalValues: Record<string, unknown>) {
37
+ console.log("Saved:", finalValues);
38
+ }
39
+ </script>
40
+
41
+ <SchemaForm
42
+ {schema}
43
+ {values}
44
+ onChange={handleChange}
45
+ showActions={true}
46
+ onSave={handleSave}
47
+ onCancel={() => console.log("Cancelled")}
48
+ />
49
+ ```
50
+ -->
51
+
52
+ <script lang="ts">
53
+ import Icon from "@iconify/svelte";
54
+ import type { ConfigSchema } from "../types/index.js";
55
+ import { FormField } from "./form/index.js";
56
+ import type { FieldSchema } from "./form/index.js";
57
+
58
+ /**
59
+ * Props interface for SchemaForm component
60
+ */
61
+ interface Props {
62
+ /**
63
+ * JSON Schema definition for the form.
64
+ * Should follow JSON Schema draft-07 format with type: "object".
65
+ */
66
+ schema: ConfigSchema;
67
+
68
+ /**
69
+ * Current form values as key-value pairs.
70
+ * Keys should correspond to properties defined in the schema.
71
+ */
72
+ values?: Record<string, unknown>;
73
+
74
+ /**
75
+ * Callback fired whenever any field value changes.
76
+ * Receives the complete updated values object.
77
+ * @param values - Updated form values
78
+ */
79
+ onChange?: (values: Record<string, unknown>) => void;
80
+
81
+ /**
82
+ * Whether to display Save and Cancel action buttons.
83
+ * @default false
84
+ */
85
+ showActions?: boolean;
86
+
87
+ /**
88
+ * Label for the save button.
89
+ * @default "Save"
90
+ */
91
+ saveLabel?: string;
92
+
93
+ /**
94
+ * Label for the cancel button.
95
+ * @default "Cancel"
96
+ */
97
+ cancelLabel?: string;
98
+
99
+ /**
100
+ * Callback fired when the Save button is clicked.
101
+ * Receives the final form values.
102
+ * @param values - Final form values
103
+ */
104
+ onSave?: (values: Record<string, unknown>) => void;
105
+
106
+ /**
107
+ * Callback fired when the Cancel button is clicked.
108
+ */
109
+ onCancel?: () => void;
110
+
111
+ /**
112
+ * Whether the form is in a loading state.
113
+ * Disables all inputs when true.
114
+ * @default false
115
+ */
116
+ loading?: boolean;
117
+
118
+ /**
119
+ * Whether the form is disabled.
120
+ * @default false
121
+ */
122
+ disabled?: boolean;
123
+
124
+ /**
125
+ * Custom CSS class for the form container.
126
+ */
127
+ class?: string;
128
+ }
129
+
130
+ let {
131
+ schema,
132
+ values = {},
133
+ onChange,
134
+ showActions = false,
135
+ saveLabel = "Save",
136
+ cancelLabel = "Cancel",
137
+ onSave,
138
+ onCancel,
139
+ loading = false,
140
+ disabled = false,
141
+ class: className = ""
142
+ }: Props = $props();
143
+
144
+ /**
145
+ * Internal reactive state for form values
146
+ */
147
+ let formValues = $state<Record<string, unknown>>({});
148
+
149
+ /**
150
+ * Initialize form values when schema or values change
151
+ * Merges default values from schema with provided values
152
+ */
153
+ $effect(() => {
154
+ if (schema?.properties) {
155
+ const mergedValues: Record<string, unknown> = {};
156
+ Object.entries(schema.properties).forEach(([key, field]) => {
157
+ const fieldConfig = field as Record<string, unknown>;
158
+ // Use provided value if available, otherwise use schema default
159
+ mergedValues[key] = values[key] !== undefined ? values[key] : fieldConfig.default;
160
+ });
161
+ formValues = mergedValues;
162
+ }
163
+ });
164
+
165
+ /**
166
+ * Check if a field is required based on schema
167
+ * @param key - Field key to check
168
+ * @returns Whether the field is required
169
+ */
170
+ function isFieldRequired(key: string): boolean {
171
+ if (!schema?.required) {
172
+ return false;
173
+ }
174
+ return schema.required.includes(key);
175
+ }
176
+
177
+ /**
178
+ * Handle field value changes from FormField components
179
+ * Updates internal state and fires onChange callback
180
+ * @param key - Field key that changed
181
+ * @param value - New field value
182
+ */
183
+ function handleFieldChange(key: string, value: unknown): void {
184
+ formValues[key] = value;
185
+
186
+ // Notify parent of the change
187
+ if (onChange) {
188
+ onChange({ ...formValues });
189
+ }
190
+ }
191
+
192
+ /**
193
+ * Handle form submission
194
+ * Collects all form values and fires onSave callback
195
+ */
196
+ function handleSave(): void {
197
+ if (loading || disabled) {
198
+ return;
199
+ }
200
+
201
+ // Collect all form values including hidden fields
202
+ const form = document.querySelector(".schema-form");
203
+ const updatedValues: Record<string, unknown> = { ...formValues };
204
+
205
+ if (form) {
206
+ const inputs = form.querySelectorAll("input, select, textarea");
207
+ inputs.forEach((input: Element) => {
208
+ const inputEl = input as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
209
+ if (inputEl.id) {
210
+ if (inputEl instanceof HTMLInputElement && inputEl.type === "checkbox") {
211
+ updatedValues[inputEl.id] = inputEl.checked;
212
+ } else if (
213
+ inputEl instanceof HTMLInputElement &&
214
+ (inputEl.type === "number" || inputEl.type === "range")
215
+ ) {
216
+ updatedValues[inputEl.id] = inputEl.value ? Number(inputEl.value) : inputEl.value;
217
+ } else if (inputEl instanceof HTMLInputElement && inputEl.type === "hidden") {
218
+ // Parse hidden field values that might be JSON
219
+ try {
220
+ const parsed = JSON.parse(inputEl.value);
221
+ updatedValues[inputEl.id] = parsed;
222
+ } catch {
223
+ // If not JSON, use raw value
224
+ updatedValues[inputEl.id] = inputEl.value;
225
+ }
226
+ } else {
227
+ updatedValues[inputEl.id] = inputEl.value;
228
+ }
229
+ }
230
+ });
231
+ }
232
+
233
+ // Preserve hidden field values from original values if not collected from form
234
+ if (values && schema?.properties) {
235
+ Object.entries(schema.properties).forEach(
236
+ ([key, property]: [string, Record<string, unknown>]) => {
237
+ if (property.format === "hidden" && !(key in updatedValues) && key in values) {
238
+ updatedValues[key] = values[key];
239
+ }
240
+ }
241
+ );
242
+ }
243
+
244
+ if (onSave) {
245
+ onSave(updatedValues);
246
+ }
247
+ }
248
+
249
+ /**
250
+ * Handle cancel action
251
+ */
252
+ function handleCancel(): void {
253
+ if (onCancel) {
254
+ onCancel();
255
+ }
256
+ }
257
+
258
+ /**
259
+ * Convert ConfigProperty to FieldSchema for FormField component
260
+ * @param property - Schema property definition
261
+ * @returns FieldSchema compatible with FormField
262
+ */
263
+ function toFieldSchema(property: Record<string, unknown>): FieldSchema {
264
+ return property as FieldSchema;
265
+ }
266
+ </script>
267
+
268
+ {#if schema?.properties}
269
+ <form
270
+ class="schema-form {className}"
271
+ class:schema-form--loading={loading}
272
+ class:schema-form--disabled={disabled}
273
+ onsubmit={(e) => {
274
+ e.preventDefault();
275
+ handleSave();
276
+ }}
277
+ >
278
+ <div class="schema-form__fields">
279
+ {#each Object.entries(schema.properties) as [key, field], index (key)}
280
+ {@const fieldSchema = toFieldSchema(field as Record<string, unknown>)}
281
+ {@const required = isFieldRequired(key)}
282
+
283
+ <FormField
284
+ fieldKey={key}
285
+ schema={fieldSchema}
286
+ value={formValues[key]}
287
+ {required}
288
+ animationIndex={index}
289
+ onChange={(val) => handleFieldChange(key, val)}
290
+ />
291
+ {/each}
292
+ </div>
293
+
294
+ {#if showActions}
295
+ <div class="schema-form__footer">
296
+ <button
297
+ type="button"
298
+ class="schema-form__button schema-form__button--secondary"
299
+ onclick={handleCancel}
300
+ disabled={loading}
301
+ >
302
+ <Icon icon="heroicons:x-mark" class="schema-form__button-icon" />
303
+ <span>{cancelLabel}</span>
304
+ </button>
305
+ <button
306
+ type="submit"
307
+ class="schema-form__button schema-form__button--primary"
308
+ disabled={loading || disabled}
309
+ >
310
+ {#if loading}
311
+ <span class="schema-form__button-spinner"></span>
312
+ {:else}
313
+ <Icon icon="heroicons:check" class="schema-form__button-icon" />
314
+ {/if}
315
+ <span>{saveLabel}</span>
316
+ </button>
317
+ </div>
318
+ {/if}
319
+ </form>
320
+ {:else}
321
+ <div class="schema-form__empty">
322
+ <div class="schema-form__empty-icon">
323
+ <Icon icon="heroicons:document-text" />
324
+ </div>
325
+ <p class="schema-form__empty-text">No schema properties defined.</p>
326
+ </div>
327
+ {/if}
328
+
329
+ <style>
330
+ /* ============================================
331
+ SCHEMA FORM - Container Styles
332
+ ============================================ */
333
+
334
+ .schema-form {
335
+ display: flex;
336
+ flex-direction: column;
337
+ gap: 1.5rem;
338
+ }
339
+
340
+ .schema-form--loading,
341
+ .schema-form--disabled {
342
+ opacity: 0.7;
343
+ pointer-events: none;
344
+ }
345
+
346
+ .schema-form__fields {
347
+ display: flex;
348
+ flex-direction: column;
349
+ gap: 1.25rem;
350
+ }
351
+
352
+ /* ============================================
353
+ FOOTER ACTIONS
354
+ ============================================ */
355
+
356
+ .schema-form__footer {
357
+ display: flex;
358
+ gap: 0.75rem;
359
+ justify-content: flex-end;
360
+ padding-top: 1rem;
361
+ border-top: 1px solid var(--color-ref-gray-100, #f3f4f6);
362
+ margin-top: 0.5rem;
363
+ }
364
+
365
+ .schema-form__button {
366
+ display: inline-flex;
367
+ align-items: center;
368
+ justify-content: center;
369
+ gap: 0.5rem;
370
+ padding: 0.625rem 1rem;
371
+ border-radius: 0.5rem;
372
+ font-size: 0.875rem;
373
+ font-weight: 600;
374
+ font-family: inherit;
375
+ cursor: pointer;
376
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
377
+ border: 1px solid transparent;
378
+ min-height: 2.5rem;
379
+ }
380
+
381
+ .schema-form__button:disabled {
382
+ opacity: 0.5;
383
+ cursor: not-allowed;
384
+ }
385
+
386
+ .schema-form__button :global(svg) {
387
+ width: 1rem;
388
+ height: 1rem;
389
+ flex-shrink: 0;
390
+ }
391
+
392
+ .schema-form__button--secondary {
393
+ background-color: #ffffff;
394
+ border-color: var(--color-ref-gray-200, #e5e7eb);
395
+ color: var(--color-ref-gray-700, #374151);
396
+ box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
397
+ }
398
+
399
+ .schema-form__button--secondary:hover:not(:disabled) {
400
+ background-color: var(--color-ref-gray-50, #f9fafb);
401
+ border-color: var(--color-ref-gray-300, #d1d5db);
402
+ color: var(--color-ref-gray-900, #111827);
403
+ }
404
+
405
+ .schema-form__button--secondary:focus-visible {
406
+ outline: none;
407
+ box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.2);
408
+ }
409
+
410
+ .schema-form__button--primary {
411
+ background: linear-gradient(
412
+ 135deg,
413
+ var(--color-ref-blue-500, #3b82f6) 0%,
414
+ var(--color-ref-blue-600, #2563eb) 100%
415
+ );
416
+ color: #ffffff;
417
+ box-shadow:
418
+ 0 1px 3px rgba(59, 130, 246, 0.3),
419
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
420
+ }
421
+
422
+ .schema-form__button--primary:hover:not(:disabled) {
423
+ background: linear-gradient(
424
+ 135deg,
425
+ var(--color-ref-blue-600, #2563eb) 0%,
426
+ var(--color-ref-blue-700, #1d4ed8) 100%
427
+ );
428
+ box-shadow:
429
+ 0 4px 12px rgba(59, 130, 246, 0.35),
430
+ inset 0 1px 0 rgba(255, 255, 255, 0.1);
431
+ transform: translateY(-1px);
432
+ }
433
+
434
+ .schema-form__button--primary:active:not(:disabled) {
435
+ transform: translateY(0);
436
+ }
437
+
438
+ .schema-form__button--primary:focus-visible {
439
+ outline: none;
440
+ box-shadow:
441
+ 0 0 0 3px rgba(59, 130, 246, 0.4),
442
+ 0 4px 12px rgba(59, 130, 246, 0.35);
443
+ }
444
+
445
+ .schema-form__button-spinner {
446
+ width: 1rem;
447
+ height: 1rem;
448
+ border: 2px solid rgba(255, 255, 255, 0.3);
449
+ border-top-color: white;
450
+ border-radius: 50%;
451
+ animation: schema-form-spin 0.6s linear infinite;
452
+ }
453
+
454
+ @keyframes schema-form-spin {
455
+ to {
456
+ transform: rotate(360deg);
457
+ }
458
+ }
459
+
460
+ /* ============================================
461
+ EMPTY STATE
462
+ ============================================ */
463
+
464
+ .schema-form__empty {
465
+ display: flex;
466
+ flex-direction: column;
467
+ align-items: center;
468
+ justify-content: center;
469
+ padding: 3rem 1.5rem;
470
+ text-align: center;
471
+ }
472
+
473
+ .schema-form__empty-icon {
474
+ width: 3rem;
475
+ height: 3rem;
476
+ margin-bottom: 1rem;
477
+ color: var(--color-ref-gray-300, #d1d5db);
478
+ }
479
+
480
+ .schema-form__empty-icon :global(svg) {
481
+ width: 100%;
482
+ height: 100%;
483
+ }
484
+
485
+ .schema-form__empty-text {
486
+ margin: 0;
487
+ font-size: 0.875rem;
488
+ color: var(--color-ref-gray-500, #6b7280);
489
+ font-style: italic;
490
+ line-height: 1.5;
491
+ }
492
+ </style>
@@ -0,0 +1,65 @@
1
+ import type { ConfigSchema } from "../types/index.js";
2
+ /**
3
+ * Props interface for SchemaForm component
4
+ */
5
+ interface Props {
6
+ /**
7
+ * JSON Schema definition for the form.
8
+ * Should follow JSON Schema draft-07 format with type: "object".
9
+ */
10
+ schema: ConfigSchema;
11
+ /**
12
+ * Current form values as key-value pairs.
13
+ * Keys should correspond to properties defined in the schema.
14
+ */
15
+ values?: Record<string, unknown>;
16
+ /**
17
+ * Callback fired whenever any field value changes.
18
+ * Receives the complete updated values object.
19
+ * @param values - Updated form values
20
+ */
21
+ onChange?: (values: Record<string, unknown>) => void;
22
+ /**
23
+ * Whether to display Save and Cancel action buttons.
24
+ * @default false
25
+ */
26
+ showActions?: boolean;
27
+ /**
28
+ * Label for the save button.
29
+ * @default "Save"
30
+ */
31
+ saveLabel?: string;
32
+ /**
33
+ * Label for the cancel button.
34
+ * @default "Cancel"
35
+ */
36
+ cancelLabel?: string;
37
+ /**
38
+ * Callback fired when the Save button is clicked.
39
+ * Receives the final form values.
40
+ * @param values - Final form values
41
+ */
42
+ onSave?: (values: Record<string, unknown>) => void;
43
+ /**
44
+ * Callback fired when the Cancel button is clicked.
45
+ */
46
+ onCancel?: () => void;
47
+ /**
48
+ * Whether the form is in a loading state.
49
+ * Disables all inputs when true.
50
+ * @default false
51
+ */
52
+ loading?: boolean;
53
+ /**
54
+ * Whether the form is disabled.
55
+ * @default false
56
+ */
57
+ disabled?: boolean;
58
+ /**
59
+ * Custom CSS class for the form container.
60
+ */
61
+ class?: string;
62
+ }
63
+ declare const SchemaForm: import("svelte").Component<Props, {}, "">;
64
+ type SchemaForm = ReturnType<typeof SchemaForm>;
65
+ export default SchemaForm;
@@ -275,3 +275,99 @@ export declare function isFieldOptionArray(options: FieldOption[] | string[]): o
275
275
  * Converts string arrays to FieldOption arrays for consistent handling
276
276
  */
277
277
  export declare function normalizeOptions(options: FieldOption[] | string[] | unknown[]): FieldOption[];
278
+ /**
279
+ * Props interface for the SchemaForm component
280
+ *
281
+ * SchemaForm is a standalone form generator that creates dynamic forms
282
+ * from JSON Schema definitions without requiring FlowDrop workflow nodes.
283
+ *
284
+ * @example
285
+ * ```typescript
286
+ * const props: SchemaFormProps = {
287
+ * schema: {
288
+ * type: "object",
289
+ * properties: {
290
+ * name: { type: "string", title: "Name" },
291
+ * age: { type: "number", title: "Age" }
292
+ * },
293
+ * required: ["name"]
294
+ * },
295
+ * values: { name: "John", age: 30 },
296
+ * onChange: (values) => console.log("Changed:", values),
297
+ * showActions: true,
298
+ * onSave: (values) => console.log("Saved:", values)
299
+ * };
300
+ * ```
301
+ */
302
+ export interface SchemaFormProps {
303
+ /**
304
+ * JSON Schema definition for the form.
305
+ * Should follow JSON Schema draft-07 format with type: "object".
306
+ * Properties define the form fields to render.
307
+ */
308
+ schema: {
309
+ type: "object";
310
+ properties: Record<string, FieldSchema>;
311
+ required?: string[];
312
+ additionalProperties?: boolean;
313
+ };
314
+ /**
315
+ * Current form values as key-value pairs.
316
+ * Keys should correspond to properties defined in the schema.
317
+ * Missing values will use schema defaults if defined.
318
+ */
319
+ values?: Record<string, unknown>;
320
+ /**
321
+ * Callback fired whenever any field value changes.
322
+ * Receives the complete updated values object.
323
+ * Use this for controlled form state management.
324
+ * @param values - Updated form values
325
+ */
326
+ onChange?: (values: Record<string, unknown>) => void;
327
+ /**
328
+ * Whether to display Save and Cancel action buttons.
329
+ * When false, the form operates in "inline" mode without buttons.
330
+ * @default false
331
+ */
332
+ showActions?: boolean;
333
+ /**
334
+ * Label text for the save button.
335
+ * Only used when showActions is true.
336
+ * @default "Save"
337
+ */
338
+ saveLabel?: string;
339
+ /**
340
+ * Label text for the cancel button.
341
+ * Only used when showActions is true.
342
+ * @default "Cancel"
343
+ */
344
+ cancelLabel?: string;
345
+ /**
346
+ * Callback fired when the Save button is clicked.
347
+ * Receives the final form values after collecting from DOM.
348
+ * @param values - Final form values
349
+ */
350
+ onSave?: (values: Record<string, unknown>) => void;
351
+ /**
352
+ * Callback fired when the Cancel button is clicked.
353
+ * Use this to reset form state or close modals.
354
+ */
355
+ onCancel?: () => void;
356
+ /**
357
+ * Whether the form is in a loading state.
358
+ * Disables all inputs and shows a loading spinner on the save button.
359
+ * @default false
360
+ */
361
+ loading?: boolean;
362
+ /**
363
+ * Whether the form is disabled.
364
+ * Prevents all interactions including save.
365
+ * @default false
366
+ */
367
+ disabled?: boolean;
368
+ /**
369
+ * Custom CSS class to apply to the form container.
370
+ * Use for additional styling customization.
371
+ */
372
+ class?: string;
373
+ }
package/dist/index.d.ts CHANGED
@@ -30,6 +30,8 @@ export { default as MarkdownDisplay } from './components/MarkdownDisplay.svelte'
30
30
  export { default as ConfigForm } from './components/ConfigForm.svelte';
31
31
  export { default as ConfigModal } from './components/ConfigModal.svelte';
32
32
  export { default as ConfigPanel } from './components/ConfigPanel.svelte';
33
+ export { default as SchemaForm } from './components/SchemaForm.svelte';
34
+ export type { SchemaFormProps, FieldSchema, FieldType, FieldFormat, FieldOption } from './components/form/types.js';
33
35
  export { default as ReadOnlyDetails } from './components/ReadOnlyDetails.svelte';
34
36
  export { default as ConnectionLine } from './components/ConnectionLine.svelte';
35
37
  export { default as LogsSidebar } from './components/LogsSidebar.svelte';
package/dist/index.js CHANGED
@@ -31,6 +31,7 @@ export { default as MarkdownDisplay } from './components/MarkdownDisplay.svelte'
31
31
  export { default as ConfigForm } from './components/ConfigForm.svelte';
32
32
  export { default as ConfigModal } from './components/ConfigModal.svelte';
33
33
  export { default as ConfigPanel } from './components/ConfigPanel.svelte';
34
+ export { default as SchemaForm } from './components/SchemaForm.svelte';
34
35
  export { default as ReadOnlyDetails } from './components/ReadOnlyDetails.svelte';
35
36
  export { default as ConnectionLine } from './components/ConnectionLine.svelte';
36
37
  export { default as LogsSidebar } from './components/LogsSidebar.svelte';
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@d34dman/flowdrop",
3
3
  "license": "MIT",
4
4
  "private": false,
5
- "version": "0.0.27",
5
+ "version": "0.0.28",
6
6
  "scripts": {
7
7
  "dev": "vite dev",
8
8
  "build": "vite build && npm run prepack",
@@ -161,4 +161,4 @@
161
161
  "static"
162
162
  ]
163
163
  }
164
- }
164
+ }