@d34dman/flowdrop 0.0.45 → 0.0.46

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 (59) hide show
  1. package/README.md +2 -2
  2. package/dist/components/ConfigForm.svelte +4 -20
  3. package/dist/components/Navbar.svelte +6 -7
  4. package/dist/components/SchemaForm.svelte +2 -10
  5. package/dist/components/WorkflowEditor.svelte +132 -4
  6. package/dist/components/form/FormAutocomplete.svelte +5 -9
  7. package/dist/components/form/FormCheckboxGroup.svelte +11 -1
  8. package/dist/components/form/FormCheckboxGroup.svelte.d.ts +2 -0
  9. package/dist/components/form/FormCodeEditor.svelte +16 -7
  10. package/dist/components/form/FormCodeEditor.svelte.d.ts +2 -0
  11. package/dist/components/form/FormField.svelte +20 -1
  12. package/dist/components/form/FormMarkdownEditor.svelte +29 -19
  13. package/dist/components/form/FormMarkdownEditor.svelte.d.ts +2 -0
  14. package/dist/components/form/FormNumberField.svelte +4 -0
  15. package/dist/components/form/FormNumberField.svelte.d.ts +2 -0
  16. package/dist/components/form/FormRangeField.svelte +4 -0
  17. package/dist/components/form/FormRangeField.svelte.d.ts +2 -0
  18. package/dist/components/form/FormSelect.svelte +4 -0
  19. package/dist/components/form/FormSelect.svelte.d.ts +2 -0
  20. package/dist/components/form/FormTemplateEditor.svelte +16 -7
  21. package/dist/components/form/FormTemplateEditor.svelte.d.ts +2 -0
  22. package/dist/components/form/FormTextField.svelte +4 -0
  23. package/dist/components/form/FormTextField.svelte.d.ts +2 -0
  24. package/dist/components/form/FormTextarea.svelte +4 -0
  25. package/dist/components/form/FormTextarea.svelte.d.ts +2 -0
  26. package/dist/components/form/FormToggle.svelte +4 -0
  27. package/dist/components/form/FormToggle.svelte.d.ts +2 -0
  28. package/dist/components/form/types.d.ts +5 -0
  29. package/dist/components/form/types.js +1 -1
  30. package/dist/components/layouts/MainLayout.svelte +5 -2
  31. package/dist/components/nodes/GatewayNode.svelte +0 -8
  32. package/dist/components/nodes/SimpleNode.svelte +2 -3
  33. package/dist/components/nodes/WorkflowNode.svelte +0 -8
  34. package/dist/components/playground/Playground.svelte +43 -38
  35. package/dist/editor/index.d.ts +3 -1
  36. package/dist/editor/index.js +5 -1
  37. package/dist/helpers/workflowEditorHelper.js +1 -2
  38. package/dist/services/autoSaveService.js +5 -5
  39. package/dist/services/historyService.d.ts +207 -0
  40. package/dist/services/historyService.js +317 -0
  41. package/dist/services/settingsService.d.ts +2 -2
  42. package/dist/services/settingsService.js +15 -21
  43. package/dist/services/toastService.d.ts +1 -1
  44. package/dist/services/toastService.js +10 -10
  45. package/dist/stores/historyStore.d.ts +133 -0
  46. package/dist/stores/historyStore.js +188 -0
  47. package/dist/stores/settingsStore.d.ts +1 -1
  48. package/dist/stores/settingsStore.js +40 -42
  49. package/dist/stores/themeStore.d.ts +2 -2
  50. package/dist/stores/themeStore.js +30 -32
  51. package/dist/stores/workflowStore.d.ts +52 -2
  52. package/dist/stores/workflowStore.js +102 -2
  53. package/dist/styles/base.css +28 -8
  54. package/dist/styles/toast.css +3 -1
  55. package/dist/styles/tokens.css +2 -2
  56. package/dist/types/settings.d.ts +3 -3
  57. package/dist/types/settings.js +13 -19
  58. package/dist/utils/colors.js +17 -17
  59. package/package.json +2 -2
package/README.md CHANGED
@@ -66,8 +66,8 @@ You get a production-ready workflow UI. You keep full control of everything else
66
66
 
67
67
  ## Features
68
68
 
69
- | | |
70
- | --------------------------- | ------------------------------------------------------------------------- |
69
+ | | |
70
+ | ---------------------------- | ------------------------------------------------------------------------- |
71
71
  | 🎨 **Visual Editor Only** | Pure UI component. No hidden backend, no external dependencies |
72
72
  | 🔐 **You Own Everything** | Your data, your servers, your orchestration logic, your security policies |
73
73
  | 🔌 **Backend Agnostic** | Connect to any API: Drupal, Laravel, Express, FastAPI, or your own |
@@ -670,11 +670,7 @@
670
670
  }
671
671
 
672
672
  .config-form__button--primary {
673
- background: linear-gradient(
674
- 135deg,
675
- var(--fd-primary) 0%,
676
- var(--fd-primary-hover) 100%
677
- );
673
+ background: linear-gradient(135deg, var(--fd-primary) 0%, var(--fd-primary-hover) 100%);
678
674
  color: var(--fd-primary-foreground);
679
675
  box-shadow:
680
676
  0 1px 3px rgba(59, 130, 246, 0.3),
@@ -682,11 +678,7 @@
682
678
  }
683
679
 
684
680
  .config-form__button--primary:hover {
685
- background: linear-gradient(
686
- 135deg,
687
- var(--fd-primary-hover) 0%,
688
- var(--fd-primary-hover) 100%
689
- );
681
+ background: linear-gradient(135deg, var(--fd-primary-hover) 0%, var(--fd-primary-hover) 100%);
690
682
  box-shadow:
691
683
  0 4px 12px rgba(59, 130, 246, 0.35),
692
684
  inset 0 1px 0 rgba(255, 255, 255, 0.1);
@@ -1006,11 +998,7 @@
1006
998
  ============================================ */
1007
999
 
1008
1000
  .config-form__button--external {
1009
- background: linear-gradient(
1010
- 135deg,
1011
- var(--fd-accent) 0%,
1012
- var(--fd-primary) 100%
1013
- );
1001
+ background: linear-gradient(135deg, var(--fd-accent) 0%, var(--fd-primary) 100%);
1014
1002
  color: var(--fd-accent-foreground);
1015
1003
  box-shadow:
1016
1004
  0 1px 3px rgba(99, 102, 241, 0.3),
@@ -1018,11 +1006,7 @@
1018
1006
  }
1019
1007
 
1020
1008
  .config-form__button--external:hover {
1021
- background: linear-gradient(
1022
- 135deg,
1023
- var(--fd-accent-hover) 0%,
1024
- var(--fd-primary-hover) 100%
1025
- );
1009
+ background: linear-gradient(135deg, var(--fd-accent-hover) 0%, var(--fd-primary-hover) 100%);
1026
1010
  box-shadow:
1027
1011
  0 4px 12px rgba(99, 102, 241, 0.35),
1028
1012
  inset 0 1px 0 rgba(255, 255, 255, 0.1);
@@ -377,12 +377,11 @@
377
377
  }
378
378
 
379
379
  .flowdrop-navbar__status {
380
- display: flex;
380
+ display: inline-flex;
381
381
  align-items: center;
382
382
  gap: 0.375rem;
383
- padding: 0.125rem 0.5rem;
383
+ padding: var(--fd-space-1) var(--fd-space-2);
384
384
  background-color: var(--fd-success-muted);
385
- border: 1px solid var(--fd-success);
386
385
  border-radius: var(--fd-radius-md);
387
386
  font-size: var(--fd-text-xs);
388
387
  font-weight: 500;
@@ -391,13 +390,13 @@
391
390
  .flowdrop-navbar__status-indicator {
392
391
  width: 0.375rem;
393
392
  height: 0.375rem;
394
- background-color: var(--fd-success);
393
+ background-color: var(--fd-success-hover);
395
394
  border-radius: 50%;
396
395
  animation: pulse 2s infinite;
397
396
  }
398
397
 
399
398
  .flowdrop-navbar__status-text {
400
- color: var(--fd-success);
399
+ color: var(--fd-success-hover);
401
400
  font-size: var(--fd-text-xs);
402
401
  font-weight: 500;
403
402
  }
@@ -705,8 +704,8 @@
705
704
  }
706
705
 
707
706
  .flowdrop-navbar__status {
708
- font-size: 0.625rem;
709
- padding: 0.125rem 0.375rem;
707
+ font-size: var(--fd-text-xs);
708
+ padding: var(--fd-space-1) var(--fd-space-2);
710
709
  }
711
710
  }
712
711
 
@@ -427,11 +427,7 @@
427
427
  }
428
428
 
429
429
  .schema-form__button--primary {
430
- background: linear-gradient(
431
- 135deg,
432
- var(--fd-primary) 0%,
433
- var(--fd-primary-hover) 100%
434
- );
430
+ background: linear-gradient(135deg, var(--fd-primary) 0%, var(--fd-primary-hover) 100%);
435
431
  color: var(--fd-primary-foreground);
436
432
  box-shadow:
437
433
  0 1px 3px rgba(59, 130, 246, 0.3),
@@ -439,11 +435,7 @@
439
435
  }
440
436
 
441
437
  .schema-form__button--primary:hover:not(:disabled) {
442
- background: linear-gradient(
443
- 135deg,
444
- var(--fd-primary-hover) 0%,
445
- var(--fd-primary-hover) 100%
446
- );
438
+ background: linear-gradient(135deg, var(--fd-primary-hover) 0%, var(--fd-primary-hover) 100%);
447
439
  box-shadow:
448
440
  0 4px 12px rgba(59, 130, 246, 0.35),
449
441
  inset 0 1px 0 rgba(255, 255, 255, 0.1);
@@ -30,6 +30,7 @@
30
30
  import type { EndpointConfig } from '../config/endpoints.js';
31
31
  import ConnectionLine from './ConnectionLine.svelte';
32
32
  import { workflowStore, workflowActions } from '../stores/workflowStore.js';
33
+ import { historyActions, setOnRestoreCallback } from '../stores/historyStore.js';
33
34
  import UniversalNode from './UniversalNode.svelte';
34
35
  import {
35
36
  EdgeStylingHelper,
@@ -65,13 +66,45 @@
65
66
  // Create a local currentWorkflow variable that we can control directly
66
67
  let currentWorkflow = $state<Workflow | null>(null);
67
68
 
69
+ // Track if we're currently dragging a node (for history debouncing)
70
+ let isDraggingNode = $state(false);
71
+
72
+ // Track the workflow ID we're currently editing to detect workflow switches
73
+ let currentWorkflowId: string | null = null;
74
+
68
75
  // Initialize currentWorkflow from global store
76
+ // Only sync when workflow ID changes (new workflow loaded) or on initial load
69
77
  $effect(() => {
70
78
  if ($workflowStore) {
71
- currentWorkflow = $workflowStore;
79
+ const storeWorkflowId = $workflowStore.id;
80
+
81
+ // Sync on initial load or when a different workflow is loaded
82
+ if (currentWorkflowId !== storeWorkflowId) {
83
+ currentWorkflow = $workflowStore;
84
+ currentWorkflowId = storeWorkflowId;
85
+ }
86
+ } else if (currentWorkflow !== null) {
87
+ // Store was cleared
88
+ currentWorkflow = null;
89
+ currentWorkflowId = null;
72
90
  }
73
91
  });
74
92
 
93
+ // Set up the history restore callback to update workflow when undo/redo is triggered
94
+ $effect(() => {
95
+ setOnRestoreCallback((restoredWorkflow: Workflow) => {
96
+ // Directly update local state (bypass store sync effect)
97
+ currentWorkflow = restoredWorkflow;
98
+ // Also update the store without triggering history
99
+ workflowActions.restoreFromHistory(restoredWorkflow);
100
+ });
101
+
102
+ // Cleanup on unmount
103
+ return () => {
104
+ setOnRestoreCallback(null);
105
+ };
106
+ });
107
+
75
108
  // Create local reactive variables that sync with currentWorkflow
76
109
  let flowNodes = $state<WorkflowNodeType[]>([]);
77
110
  let flowEdges = $state<WorkflowEdge[]>([]);
@@ -282,6 +315,29 @@
282
315
  // Handle arrows in our custom connection handler
283
316
  const defaultEdgeOptions = {};
284
317
 
318
+ /**
319
+ * Handle node drag start
320
+ *
321
+ * Marks the beginning of a drag operation.
322
+ */
323
+ function handleNodeDragStart(): void {
324
+ isDraggingNode = true;
325
+ }
326
+
327
+ /**
328
+ * Handle node drag stop
329
+ *
330
+ * Push the NEW state (after drag) to history.
331
+ * Undo will then restore to the previous state (before drag).
332
+ */
333
+ function handleNodeDragStop(): void {
334
+ isDraggingNode = false;
335
+ // Push the current state AFTER the drag completed
336
+ if (currentWorkflow) {
337
+ workflowActions.pushHistory('Move node', currentWorkflow);
338
+ }
339
+ }
340
+
285
341
  /**
286
342
  * Handle new connections between nodes
287
343
  * Let SvelteFlow handle edge creation, styling will be applied via reactive effects
@@ -303,6 +359,12 @@
303
359
  if (currentWorkflow) {
304
360
  updateCurrentWorkflowFromSvelteFlow();
305
361
  }
362
+
363
+ // Push to history AFTER the connection is made
364
+ // This way undo will restore to the state before the connection
365
+ if (currentWorkflow) {
366
+ workflowActions.pushHistory('Add connection', currentWorkflow);
367
+ }
306
368
  }
307
369
 
308
370
  /**
@@ -338,15 +400,18 @@
338
400
 
339
401
  // Show native confirmation dialog
340
402
  const confirmed = window.confirm(message);
341
- return confirmed;
403
+ if (!confirmed) {
404
+ return false;
405
+ }
342
406
  }
343
407
 
344
- // If confirmDelete is disabled, proceed with deletion
408
+ // Don't push to history here - we'll push AFTER deletion in handleNodesDelete
409
+ // This ensures undo will restore the state before deletion
345
410
  return true;
346
411
  }
347
412
 
348
413
  /**
349
- * Handle node deletion - automatically remove connected edges
414
+ * Handle node deletion - automatically remove connected edges and push to history
350
415
  */
351
416
  function handleNodesDelete(params: { nodes: WorkflowNodeType[]; edges: WorkflowEdge[] }): void {
352
417
  const deletedNodeIds = new Set(params.nodes.map((node) => node.id));
@@ -360,6 +425,21 @@
360
425
  if (currentWorkflow) {
361
426
  updateCurrentWorkflowFromSvelteFlow();
362
427
  }
428
+
429
+ // Push to history AFTER the deletion so undo restores the previous state
430
+ const nodeCount = params.nodes.length;
431
+ const edgeCount = params.edges.length;
432
+ let description = 'Delete';
433
+ if (nodeCount > 0 && edgeCount > 0) {
434
+ description = `Delete ${nodeCount} node${nodeCount > 1 ? 's' : ''} and ${edgeCount} connection${edgeCount > 1 ? 's' : ''}`;
435
+ } else if (nodeCount > 0) {
436
+ description = `Delete ${nodeCount} node${nodeCount > 1 ? 's' : ''}`;
437
+ } else if (edgeCount > 0) {
438
+ description = `Delete ${edgeCount} connection${edgeCount > 1 ? 's' : ''}`;
439
+ }
440
+ if (currentWorkflow) {
441
+ workflowActions.pushHistory(description, currentWorkflow);
442
+ }
363
443
  }
364
444
 
365
445
  /**
@@ -413,6 +493,7 @@
413
493
  const newNode = NodeOperationsHelper.createNodeFromDrop(nodeTypeData, position, flowNodes);
414
494
 
415
495
  if (newNode && currentWorkflow) {
496
+ // Add the node first
416
497
  currentWorkflow = WorkflowOperationsHelper.addNode(currentWorkflow, newNode);
417
498
 
418
499
  // Update the global store
@@ -420,6 +501,10 @@
420
501
 
421
502
  // Wait for DOM update to ensure SvelteFlow updates
422
503
  await tick();
504
+
505
+ // Push to history AFTER adding the node
506
+ // This way undo will restore to the state before the add
507
+ workflowActions.pushHistory('Add node', currentWorkflow);
423
508
  } else if (!currentWorkflow) {
424
509
  console.warn('No currentWorkflow available for new node');
425
510
  }
@@ -450,8 +535,49 @@
450
535
  function handleEdgeRefreshComplete(): void {
451
536
  nodeIdToRefresh = null;
452
537
  }
538
+
539
+ /**
540
+ * Handle keyboard shortcuts for undo/redo
541
+ *
542
+ * - Ctrl+Z (or Cmd+Z on Mac): Undo
543
+ * - Ctrl+Shift+Z (or Cmd+Shift+Z): Redo
544
+ * - Ctrl+Y (or Cmd+Y): Redo (Windows convention)
545
+ */
546
+ function handleKeydown(event: KeyboardEvent): void {
547
+ // Check for Ctrl (Windows/Linux) or Cmd (Mac)
548
+ const isModifierPressed = event.ctrlKey || event.metaKey;
549
+
550
+ if (!isModifierPressed) {
551
+ return;
552
+ }
553
+
554
+ // Don't handle shortcuts if user is typing in an input, textarea, or contenteditable
555
+ const target = event.target as HTMLElement;
556
+ const isInputElement =
557
+ target.tagName === 'INPUT' || target.tagName === 'TEXTAREA' || target.isContentEditable;
558
+
559
+ if (isInputElement) {
560
+ return;
561
+ }
562
+
563
+ // Undo: Ctrl+Z (without Shift)
564
+ if (event.key === 'z' && !event.shiftKey) {
565
+ event.preventDefault();
566
+ historyActions.undo();
567
+ return;
568
+ }
569
+
570
+ // Redo: Ctrl+Shift+Z or Ctrl+Y
571
+ if ((event.key === 'z' && event.shiftKey) || event.key === 'y') {
572
+ event.preventDefault();
573
+ historyActions.redo();
574
+ return;
575
+ }
576
+ }
453
577
  </script>
454
578
 
579
+ <svelte:window onkeydown={handleKeydown} />
580
+
455
581
  <SvelteFlowProvider>
456
582
  <!-- EdgeRefresher component - handles updateNodeInternals calls -->
457
583
  <EdgeRefresher {nodeIdToRefresh} onRefreshComplete={handleEdgeRefreshComplete} />
@@ -471,6 +597,8 @@
471
597
  onconnect={handleConnect}
472
598
  onbeforedelete={handleBeforeDelete}
473
599
  ondelete={handleNodesDelete}
600
+ onnodedragstart={handleNodeDragStart}
601
+ onnodedragstop={handleNodeDragStop}
474
602
  minZoom={0.2}
475
603
  maxZoom={3}
476
604
  clickConnect={true}
@@ -262,7 +262,7 @@
262
262
  function handleInput(event: Event): void {
263
263
  const target = event.currentTarget as HTMLInputElement;
264
264
  inputValue = target.value;
265
-
265
+
266
266
  // Open dropdown
267
267
  showDropdown();
268
268
 
@@ -493,9 +493,9 @@
493
493
  */
494
494
  function showDropdown(): void {
495
495
  if (!popoverElement || disabled) return;
496
-
496
+
497
497
  updatePopoverPosition();
498
-
498
+
499
499
  try {
500
500
  popoverElement.showPopover();
501
501
  isOpen = true;
@@ -510,7 +510,7 @@
510
510
  */
511
511
  function hideDropdown(): void {
512
512
  if (!popoverElement) return;
513
-
513
+
514
514
  try {
515
515
  popoverElement.hidePopover();
516
516
  } catch {
@@ -658,11 +658,7 @@
658
658
  style={popoverStyle}
659
659
  onmousedown={(e) => e.preventDefault()}
660
660
  >
661
- <ul
662
- class="form-autocomplete__listbox"
663
- role="listbox"
664
- aria-label="Suggestions"
665
- >
661
+ <ul class="form-autocomplete__listbox" role="listbox" aria-label="Suggestions">
666
662
  {#if isLoading}
667
663
  <li class="form-autocomplete__status form-autocomplete__status--loading">
668
664
  <Icon icon="heroicons:arrow-path" class="form-autocomplete__status-icon" />
@@ -18,13 +18,22 @@
18
18
  value: string[];
19
19
  /** Available options */
20
20
  options: string[];
21
+ /** Whether the field is disabled (read-only) */
22
+ disabled?: boolean;
21
23
  /** ARIA description ID */
22
24
  ariaDescribedBy?: string;
23
25
  /** Callback when value changes */
24
26
  onChange: (value: string[]) => void;
25
27
  }
26
28
 
27
- let { id, value = [], options = [], ariaDescribedBy, onChange }: Props = $props();
29
+ let {
30
+ id,
31
+ value = [],
32
+ options = [],
33
+ disabled = false,
34
+ ariaDescribedBy,
35
+ onChange
36
+ }: Props = $props();
28
37
 
29
38
  /**
30
39
  * Handle checkbox toggle
@@ -56,6 +65,7 @@
56
65
  class="form-checkbox__input"
57
66
  value={option}
58
67
  checked={isChecked}
68
+ {disabled}
59
69
  onchange={(e) => handleCheckboxChange(option, e.currentTarget.checked)}
60
70
  />
61
71
  <span class="form-checkbox__custom" aria-hidden="true">
@@ -5,6 +5,8 @@ interface Props {
5
5
  value: string[];
6
6
  /** Available options */
7
7
  options: string[];
8
+ /** Whether the field is disabled (read-only) */
9
+ disabled?: boolean;
8
10
  /** ARIA description ID */
9
11
  ariaDescribedBy?: string;
10
12
  /** Callback when value changes */
@@ -47,6 +47,8 @@
47
47
  height?: string;
48
48
  /** Whether to auto-format JSON on blur */
49
49
  autoFormat?: boolean;
50
+ /** Whether the field is disabled (read-only) */
51
+ disabled?: boolean;
50
52
  /** ARIA description ID */
51
53
  ariaDescribedBy?: string;
52
54
  /** Callback when value changes */
@@ -61,6 +63,7 @@
61
63
  darkTheme = false,
62
64
  height = '200px',
63
65
  autoFormat = true,
66
+ disabled = false,
64
67
  ariaDescribedBy,
65
68
  onChange
66
69
  }: Props = $props();
@@ -167,6 +170,7 @@
167
170
  /**
168
171
  * Create editor extensions array
169
172
  * Uses minimal setup for better performance (no auto-closing brackets, no autocompletion)
173
+ * When disabled is true, adds readOnly/editable so the editor cannot be modified
170
174
  */
171
175
  function createExtensions() {
172
176
  const extensions = [
@@ -177,22 +181,27 @@
177
181
  highlightActiveLine(),
178
182
  drawSelection(),
179
183
 
180
- // Editing features
181
- history(),
182
- indentOnInput(),
184
+ // Editing features (skip when read-only)
185
+ ...(disabled
186
+ ? []
187
+ : [
188
+ history(),
189
+ indentOnInput(),
190
+ keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab])
191
+ ]),
192
+
193
+ // Read-only: prevent document changes and mark content as non-editable
194
+ ...(disabled ? [EditorState.readOnly.of(true), EditorView.editable.of(false)] : []),
183
195
 
184
196
  // Syntax highlighting
185
197
  syntaxHighlighting(defaultHighlightStyle, { fallback: true }),
186
198
 
187
- // Keymaps for basic editing
188
- keymap.of([...defaultKeymap, ...historyKeymap, indentWithTab]),
189
-
190
199
  // JSON-specific features
191
200
  json(),
192
201
  linter(jsonParseLinter()),
193
202
  lintGutter(),
194
203
 
195
- // Update listener
204
+ // Update listener (only fires on user edit when not disabled)
196
205
  EditorView.updateListener.of(handleUpdate),
197
206
 
198
207
  // Custom theme
@@ -13,6 +13,8 @@ interface Props {
13
13
  height?: string;
14
14
  /** Whether to auto-format JSON on blur */
15
15
  autoFormat?: boolean;
16
+ /** Whether the field is disabled (read-only) */
17
+ disabled?: boolean;
16
18
  /** ARIA description ID */
17
19
  ariaDescribedBy?: string;
18
20
  /** Callback when value changes */
@@ -61,6 +61,11 @@
61
61
 
62
62
  let { fieldKey, schema, value, required = false, animationIndex = 0, onChange }: Props = $props();
63
63
 
64
+ /**
65
+ * When schema.readOnly is true, disable all inputs (no editing).
66
+ */
67
+ const isReadOnly = $derived(schema.readOnly === true);
68
+
64
69
  /**
65
70
  * Computed description ID for ARIA association
66
71
  */
@@ -204,7 +209,7 @@
204
209
  }
205
210
  return value ? [String(value)] : [];
206
211
  }
207
- return String(value ?? "");
212
+ return String(value ?? '');
208
213
  });
209
214
  </script>
210
215
 
@@ -222,6 +227,7 @@
222
227
  value={arrayValue}
223
228
  options={enumOptions}
224
229
  ariaDescribedBy={descriptionId}
230
+ disabled={isReadOnly}
225
231
  onChange={(val) => onChange(val)}
226
232
  />
227
233
  {:else if fieldType === 'select-enum'}
@@ -231,6 +237,7 @@
231
237
  options={enumOptions}
232
238
  {required}
233
239
  ariaDescribedBy={descriptionId}
240
+ disabled={isReadOnly}
234
241
  onChange={(val) => onChange(val)}
235
242
  />
236
243
  {:else if fieldType === 'textarea'}
@@ -240,6 +247,7 @@
240
247
  placeholder={schema.placeholder ?? ''}
241
248
  {required}
242
249
  ariaDescribedBy={descriptionId}
250
+ disabled={isReadOnly}
243
251
  onChange={(val) => onChange(val)}
244
252
  />
245
253
  {:else if fieldType === 'text'}
@@ -249,6 +257,7 @@
249
257
  placeholder={schema.placeholder ?? ''}
250
258
  {required}
251
259
  ariaDescribedBy={descriptionId}
260
+ disabled={isReadOnly}
252
261
  onChange={(val) => onChange(val)}
253
262
  />
254
263
  {:else if fieldType === 'number'}
@@ -261,6 +270,7 @@
261
270
  step={schema.step}
262
271
  {required}
263
272
  ariaDescribedBy={descriptionId}
273
+ disabled={isReadOnly}
264
274
  onChange={(val) => onChange(val)}
265
275
  />
266
276
  {:else if fieldType === 'range'}
@@ -272,6 +282,7 @@
272
282
  step={schema.step}
273
283
  {required}
274
284
  ariaDescribedBy={descriptionId}
285
+ disabled={isReadOnly}
275
286
  onChange={(val) => onChange(val)}
276
287
  />
277
288
  {:else if fieldType === 'toggle'}
@@ -279,6 +290,7 @@
279
290
  id={fieldKey}
280
291
  value={booleanValue}
281
292
  ariaDescribedBy={descriptionId}
293
+ disabled={isReadOnly}
282
294
  onChange={(val) => onChange(val)}
283
295
  />
284
296
  {:else if fieldType === 'select-options'}
@@ -288,6 +300,7 @@
288
300
  options={selectOptions}
289
301
  {required}
290
302
  ariaDescribedBy={descriptionId}
303
+ disabled={isReadOnly}
291
304
  onChange={(val) => onChange(val)}
292
305
  />
293
306
  {:else if fieldType === 'array' && schema.items}
@@ -298,6 +311,7 @@
298
311
  minItems={schema.minItems}
299
312
  maxItems={schema.maxItems}
300
313
  addLabel={`Add ${schema.items.title ?? 'Item'}`}
314
+ disabled={isReadOnly}
301
315
  onChange={(val) => onChange(val)}
302
316
  />
303
317
  {:else if fieldType === 'code-editor'}
@@ -310,6 +324,7 @@
310
324
  darkTheme={(schema.darkTheme as boolean | undefined) ?? false}
311
325
  autoFormat={(schema.autoFormat as boolean | undefined) ?? true}
312
326
  ariaDescribedBy={descriptionId}
327
+ disabled={isReadOnly}
313
328
  onChange={(val) => onChange(val)}
314
329
  />
315
330
  {:else if fieldType === 'markdown-editor'}
@@ -323,6 +338,7 @@
323
338
  showStatusBar={(schema.showStatusBar as boolean | undefined) ?? true}
324
339
  spellChecker={(schema.spellChecker as boolean | undefined) ?? false}
325
340
  ariaDescribedBy={descriptionId}
341
+ disabled={isReadOnly}
326
342
  onChange={(val) => onChange(val)}
327
343
  />
328
344
  {:else if fieldType === 'template-editor'}
@@ -338,6 +354,7 @@
338
354
  placeholderExample={(schema.placeholderExample as string | undefined) ??
339
355
  'Hello {{ name }}, your order #{{ order_id }} is ready!'}
340
356
  ariaDescribedBy={descriptionId}
357
+ disabled={isReadOnly}
341
358
  onChange={(val) => onChange(val)}
342
359
  />
343
360
  {:else if fieldType === 'autocomplete' && schema.autocomplete}
@@ -348,6 +365,7 @@
348
365
  placeholder={schema.placeholder ?? ''}
349
366
  {required}
350
367
  ariaDescribedBy={descriptionId}
368
+ disabled={isReadOnly}
351
369
  onChange={(val) => onChange(val)}
352
370
  />
353
371
  {:else}
@@ -357,6 +375,7 @@
357
375
  value={stringValue}
358
376
  placeholder={schema.placeholder ?? ''}
359
377
  ariaDescribedBy={descriptionId}
378
+ disabled={isReadOnly}
360
379
  onChange={(val) => onChange(val)}
361
380
  />
362
381
  {/if}