@d34dman/flowdrop 0.0.44 → 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 (67) 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/NodeSidebar.svelte +6 -2
  5. package/dist/components/SchemaForm.svelte +2 -10
  6. package/dist/components/WorkflowEditor.svelte +143 -13
  7. package/dist/components/form/FormAutocomplete.svelte +5 -9
  8. package/dist/components/form/FormCheckboxGroup.svelte +11 -1
  9. package/dist/components/form/FormCheckboxGroup.svelte.d.ts +2 -0
  10. package/dist/components/form/FormCodeEditor.svelte +16 -7
  11. package/dist/components/form/FormCodeEditor.svelte.d.ts +2 -0
  12. package/dist/components/form/FormField.svelte +20 -1
  13. package/dist/components/form/FormMarkdownEditor.svelte +29 -19
  14. package/dist/components/form/FormMarkdownEditor.svelte.d.ts +2 -0
  15. package/dist/components/form/FormNumberField.svelte +4 -0
  16. package/dist/components/form/FormNumberField.svelte.d.ts +2 -0
  17. package/dist/components/form/FormRangeField.svelte +4 -0
  18. package/dist/components/form/FormRangeField.svelte.d.ts +2 -0
  19. package/dist/components/form/FormSelect.svelte +4 -0
  20. package/dist/components/form/FormSelect.svelte.d.ts +2 -0
  21. package/dist/components/form/FormTemplateEditor.svelte +16 -7
  22. package/dist/components/form/FormTemplateEditor.svelte.d.ts +2 -0
  23. package/dist/components/form/FormTextField.svelte +4 -0
  24. package/dist/components/form/FormTextField.svelte.d.ts +2 -0
  25. package/dist/components/form/FormTextarea.svelte +4 -0
  26. package/dist/components/form/FormTextarea.svelte.d.ts +2 -0
  27. package/dist/components/form/FormToggle.svelte +4 -0
  28. package/dist/components/form/FormToggle.svelte.d.ts +2 -0
  29. package/dist/components/form/types.d.ts +5 -0
  30. package/dist/components/form/types.js +1 -1
  31. package/dist/components/layouts/MainLayout.svelte +5 -2
  32. package/dist/components/nodes/GatewayNode.svelte +99 -86
  33. package/dist/components/nodes/IdeaNode.svelte +20 -35
  34. package/dist/components/nodes/NotesNode.svelte +6 -2
  35. package/dist/components/nodes/SimpleNode.svelte +32 -31
  36. package/dist/components/nodes/SquareNode.svelte +35 -45
  37. package/dist/components/nodes/TerminalNode.svelte +25 -61
  38. package/dist/components/nodes/ToolNode.svelte +36 -18
  39. package/dist/components/nodes/WorkflowNode.svelte +97 -73
  40. package/dist/components/playground/Playground.svelte +43 -38
  41. package/dist/editor/index.d.ts +3 -1
  42. package/dist/editor/index.js +5 -1
  43. package/dist/helpers/nodeLayoutHelper.d.ts +14 -0
  44. package/dist/helpers/nodeLayoutHelper.js +19 -0
  45. package/dist/helpers/workflowEditorHelper.js +1 -2
  46. package/dist/services/autoSaveService.js +5 -5
  47. package/dist/services/historyService.d.ts +207 -0
  48. package/dist/services/historyService.js +317 -0
  49. package/dist/services/settingsService.d.ts +2 -2
  50. package/dist/services/settingsService.js +15 -21
  51. package/dist/services/toastService.d.ts +1 -1
  52. package/dist/services/toastService.js +10 -10
  53. package/dist/stores/historyStore.d.ts +133 -0
  54. package/dist/stores/historyStore.js +188 -0
  55. package/dist/stores/settingsStore.d.ts +1 -1
  56. package/dist/stores/settingsStore.js +40 -42
  57. package/dist/stores/themeStore.d.ts +2 -2
  58. package/dist/stores/themeStore.js +30 -32
  59. package/dist/stores/workflowStore.d.ts +52 -2
  60. package/dist/stores/workflowStore.js +102 -2
  61. package/dist/styles/base.css +67 -7
  62. package/dist/styles/toast.css +3 -1
  63. package/dist/styles/tokens.css +38 -2
  64. package/dist/types/settings.d.ts +3 -3
  65. package/dist/types/settings.js +13 -19
  66. package/dist/utils/colors.js +18 -18
  67. package/package.json +1 -1
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
 
@@ -598,7 +598,7 @@
598
598
  width: 2rem;
599
599
  height: 2rem;
600
600
  border-radius: 0.5rem;
601
- background: color-mix(in srgb, var(--_icon-color) 15%, transparent);
601
+ background: color-mix(in srgb, var(--_icon-color) var(--fd-node-icon-bg-opacity), transparent);
602
602
  color: var(--fd-node-icon);
603
603
  font-size: var(--fd-text-sm);
604
604
  display: flex;
@@ -610,7 +610,11 @@
610
610
 
611
611
  .flowdrop-node-item:hover .flowdrop-node-icon,
612
612
  .flowdrop-details__summary:hover .flowdrop-node-icon {
613
- background: color-mix(in srgb, var(--_icon-color) 22%, transparent);
613
+ background: color-mix(
614
+ in srgb,
615
+ var(--_icon-color) var(--fd-node-icon-bg-opacity-hover),
616
+ transparent
617
+ );
614
618
  transform: scale(1.05);
615
619
  }
616
620
 
@@ -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,
@@ -39,8 +40,8 @@
39
40
  } from '../helpers/workflowEditorHelper.js';
40
41
  import type { NodeExecutionInfo } from '../types/index.js';
41
42
  import { areNodeArraysEqual, areEdgeArraysEqual, throttle } from '../utils/performanceUtils.js';
42
- import { Toaster } from "svelte-5-french-toast";
43
- import { flowdropToastOptions, FLOWDROP_TOASTER_CLASS } from "../services/toastService.js";
43
+ import { Toaster } from 'svelte-5-french-toast';
44
+ import { flowdropToastOptions, FLOWDROP_TOASTER_CLASS } from '../services/toastService.js';
44
45
 
45
46
  interface Props {
46
47
  nodes?: NodeMetadata[];
@@ -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}
@@ -530,10 +658,10 @@
530
658
 
531
659
  <!-- Toast notifications container -->
532
660
  <Toaster
533
- position="bottom-center"
534
- containerClassName={FLOWDROP_TOASTER_CLASS}
535
- toastOptions={flowdropToastOptions}
536
- />
661
+ position="bottom-center"
662
+ containerClassName={FLOWDROP_TOASTER_CLASS}
663
+ toastOptions={flowdropToastOptions}
664
+ />
537
665
 
538
666
  <style>
539
667
  .flowdrop-workflow-editor {
@@ -625,10 +753,10 @@
625
753
  fill: currentColor;
626
754
  }
627
755
 
756
+ /* Handle size/position only; colors come from inline --fd-handle-fill and base.css ::before */
628
757
  :global(.flowdrop-workflow-editor .svelte-flow__handle) {
629
- width: 18px;
630
- height: 18px;
631
- border: 2px solid white;
758
+ width: var(--fd-handle-size);
759
+ height: var(--fd-handle-size);
632
760
  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
633
761
  z-index: 20;
634
762
  }
@@ -637,6 +765,8 @@
637
765
  :global(.flowdrop-workflow-editor .svelte-flow__handle) {
638
766
  pointer-events: all;
639
767
  cursor: crosshair;
768
+ background-color: var(--fd-handle-fill);
769
+ border-color: var(--fd-handle-border-color);
640
770
  }
641
771
 
642
772
  /**
@@ -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 */